
OpenClaw 是对 Monolith 1997 年平台游戏《Captain Claw》的开源引擎重实现。原版是 Windows 95 时代的 16 位色 DirectDraw 程序,资源封在 CLAW.REZ 归档里;OpenClaw 用 C++ 与 SDL2 重写整套运行时,读取原版资源文件运行,从而把一只海盗猫带到了 Linux、macOS 与现代 Windows 上,并支持 1080p、自定义关卡与 MOD。它不是模拟器,而是"资源兼容的引擎重写"——这个定位决定了它的架构。
代码按职责分为四层,层间只允许单向依赖:
RezArchive 解析 REZ 归档,按路径取出精灵图、关卡、音效、字体;这种切分让"换渲染后端"或"加新关卡格式"都不必动游戏逻辑,也是它能被社区持续 MOD 化改造的前提。
REZ 是 Monolith 自有的归档格式,本质是"头部 + 目录表 + 数据区"。引擎启动时先建索引,把路径映射到偏移与长度,之后所有资源按名取用。
// 示意:REZ 归档索引构建
class RezArchive {
public:
bool open(const std::string& path) {
m_file.open(path, std::ios::binary);
if (!m_file) return false;
Header hdr{};
m_file.read(reinterpret_cast<char*>(&hdr), sizeof(hdr));
if (std::memcmp(hdr.magic, "REZ", 3) != 0) return false;
// 目录表:条目数 + 每项(路径、偏移、大小)
m_file.seekg(hdr.dir_offset);
uint32_t count = 0;
m_file.read(reinterpret_cast<char*>(&count), sizeof(count));
for (uint32_t i = 0; i < count; ++i) {
Entry e{};
uint16_t nameLen = 0;
m_file.read(reinterpret_cast<char*>(&nameLen), sizeof(nameLen));
e.path.resize(nameLen);
m_file.read(e.path.data(), nameLen);
m_file.read(reinterpret_cast<char*>(&e.offset), sizeof(e.offset));
m_file.read(reinterpret_cast<char*>(&e.size), sizeof(e.size));
m_index[normalize(e.path)] = e; // 大小写不敏感、统一分隔符
}
return true;
}
std::vector<uint8_t> read(const std::string& path) const {
auto it = m_index.find(normalize(path));
if (it == m_index.end()) return {};
std::vector<uint8_t> buf(it->second.size);
m_file.seekg(it->second.offset);
m_file.read(reinterpret_cast<char*>(buf.data()), buf.size());
return buf;
}
private:
struct Entry { std::string path; uint32_t offset, size; };
mutable std::ifstream m_file;
std::unordered_map<std::string, Entry> m_index;
};路径归一化是必须的:原版资源里大小写与反斜杠混用,不统一就会"资源明明存在却加载失败"。
平台游戏的手感极度依赖确定性物理,因此主循环采用固定时间步累加器:渲染帧率可变,逻辑步长恒定为 1/60 秒,避免高刷屏下跳跃高度不一致。
constexpr double STEP = 1.0 / 60.0;
void Engine::run() {
double acc = 0.0;
uint64_t prev = SDL_GetTicks64();
while (!m_quit) {
uint64_t now = SDL_GetTicks64();
double frame = std::min((now - prev) / 1000.0, 0.25); // 防死亡螺旋
prev = now;
acc += frame;
pollEvents(); // SDL_PollEvent → 输入状态
while (acc >= STEP) {
update(STEP); // 逻辑:移动、碰撞、状态机
acc -= STEP;
}
render(acc / STEP); // 传插值系数,做平滑渲染
SDL_GL_SwapWindow(m_window);
}
}Claw 的行为是典型的状态机:待机、行走、跳跃、攀爬、游泳、攻击、受伤、死亡。每个状态负责自己的动画帧推进与转移条件,输入只产生"意图",由状态决定是否接受。
class ClawState {
public:
virtual ~ClawState() = default;
virtual void enter(Claw& c) {}
virtual ClawState* update(Claw& c, const Input& in, double dt) = 0;
};
class JumpState : public ClawState {
public:
ClawState* update(Claw& c, const Input& in, double dt) override {
c.vel.y += GRAVITY * dt; // 重力积分
c.pos += c.vel * dt;
if (c.onGround()) return &g_idle; // 落地 → 待机
if (in.attack && c.canAttack())
return &g_attack;
return this;
}
};
// 碰撞:与关卡 tile 做 AABB 分轴求解,先 X 后 Y 避免"贴墙卡住"
void resolve(Claw& c, const Level& lv, double dt) {
c.pos.x += c.vel.x * dt;
for (auto& t : lv.solidTilesNear(c.aabb()))
if (intersects(c.aabb(), t)) c.pos.x = pushOutX(c, t), c.vel.x = 0;
c.pos.y += c.vel.y * dt;
c.onGround = false;
for (auto& t : lv.solidTilesNear(c.aabb()))
if (intersects(c.aabb(), t)) {
c.pos.y = pushOutY(c, t);
if (c.vel.y > 0) c.onGround = true;
c.vel.y = 0;
}
}分轴求解是 2D 平台游戏的标准做法:合轴求解会让角色在斜坡与墙角处抖动或穿模。
OpenClaw 的社区活力来自可扩展性。关卡以文本/二进制资源描述 tile 布局与实体出生点,MOD 只需按约定放入覆盖目录,资源层优先读覆盖目录、其次读 REZ,逻辑层完全无感。自定义精灵则通过调色板替换实现——原版是 8 位索引色,换一张 PAL 就能整体换肤,成本极低。
依赖只有 SDL2、SDL2_image、SDL2_mixer 与 CMake,Windows 用 MSVC、Linux 用 GCC、macOS 用 Clang 均可直接构建。移植新平台的工作量集中在平台层:把窗口、音频回调、定时器映射到目标系统,游戏层一行不改——这正是分层架构的红利。
OpenClaw 的价值不在"复刻一只猫",而在示范了一种低成本复活老游戏的工程路径:以资源格式为契约,重写运行时,把平台差异压到最薄的一层。对做引擎、做兼容层、做老资源再利用的开发者来说,它是一份可读、可改、可运行的活教材。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。