DungeonRush Logo

收集英雄,壮大队伍,收集武器,击败怪物,探索地牢

DungeonRush 是我做的一款开源游戏。

那一阵子又和室友打元气骑士打得比较多,于是就想,能不能让 英雄 作为贪吃蛇的节,然后做一个 Roughlike 地牢闯关的游戏呢?

于是就有了 DungeonRush元气贪吃蛇(划掉)

游戏有四种角色(都在菜单界面显示出来啦),他们的血量、攻击方式各不相同。怪物死亡后还有概率掉落武器。所以他们还可以装备不同的武器。比如法师甚至可以捡起 boss 武器,进行范围 AOE 落雷。骑士可以装备圣剑,攻击有概率触发全队受到伤害减半,免疫一切负面效果的圣盾Buff

甚至支持本机双人对战。和室友实测可玩性较高。

v2ex Post

Source Code

下面是交给老师的设计报告 :P

需求分析

图形需求

  1. 显示 GUI 界面,开始游戏,选择关卡, 排行榜
  2. 加载图片、文字
  3. 显示一定时长的动画,可以单次或无限循环播放
  4. 显示文字
  5. 子弹飞行、爆炸动画

游戏需求

  1. 攻击判定 碰撞检测
  2. 角色:血量,武器(攻速、攻击方式(单体/群体,近程/远程,动画效果))
  3. 敌人AI:移动,自动攻击(方向)
  4. 地图自动生成(元胞自动机)
  5. 控制
  6. 难度设定(移动速度,敌人血量、攻速,敌人数量、类型)
  7. 地刺,收放动画

系统设计

classDiagram
    LinkNode o-- LinkList
    LinkList |>-- AnimationList
    Animation o-- AnimationList
    Texture *-- Animation
    Effect *-- Animation
    SDLTexture*-- Texture
    class LinkNode {
        void* element
        LinkNode *nxt, *pre
    }
    class Animation {
        renderAnimation()
    }
    class AnimationList {
        render()
    }
    Weapon *-- Sprite
    Sprite o-- Snake
    LinkList *-- Snake

    BaseUi |>-- MainUi
    BaseUi |>-- RankListUi
    RankListUi |>-- LocalRankListUi

    Block |>-- Trap
    Block |>-- Floor

ADT

用 void* 实现泛型编程

struct _LinkNode {
  void* element;
  struct _LinkNode *pre, *nxt;
};
typedef struct _LinkNode LinkNode;
typedef struct {
  LinkNode *head, *tail;
} LinkList;

算法和程序流程

见图解

模块和接口

视图、业务逻辑分离

res.c 图形界面初始化和资源加载模块

render.c 渲染模块

game.c 游戏核心逻辑模块

ai.c AI 决策模块

map.c 地图(元胞自动机)模块

helper.c 数学函数和几何判定模块

audio.c 音效和 BGM 模块

ui.c UI 模块

storage.c 数据储存(记录排行榜)模块

snake.c 程序入口

界面设计

调用渲染模块使用游戏中的元素直接渲染界面

用函数递归调用模拟基类派生

ui.c

系统实现

测试与调试

vscode + valgrind + coredump

心得与体会

国庆节前后,我听说我们有一门贪吃蛇课设。我认为这是一次锻炼自己的很好的机会。参加算法竞赛让我积累了十几万行的代码经验,但缺乏中大型项目的实践经验。算法竞赛中最复杂的程序不过三四百行,并且不关心图形界面,只关心程序的运行效率、算法的时空复杂度。算法和数据结构是软件设计的核心,但绝不是全部。因此我决心写好一个界面美观、架构良好、符合工程规范的贪吃蛇。

只写贪吃蛇太无趣,像素风手机游戏元气骑士给了我启发:贪吃蛇,是一些方块的队列。那我为什么不能让每一个方块都是一个角色,用角色的队列代替传统的蛇,让他们攻击怪物,探索地牢?

首先我在 Google 上收集像素图片资源,找不到的就自己画、朋友画。游戏中的弓箭、法杖、能量球、冰锥等等都是原创。

做好准备之后,可以开工了。

我先学习了 SDL 提供给我的处理图形的方式,写出一个小人在地板上走来走去的 Demo 验证效果之后,开始思考如何架构程序。

我想起我曾经听过的几句简短的程序设计思想:“强内聚,低耦合”、”业务逻辑和渲染分离”。其中对我早期设计架构影响最大的是后者。

SDL 提供给我的功能有限,一次只能只能显示一张静态图片。我必须要写一个自己的游戏渲染引擎。网上的像素资源包一般是 Spritesheet 的形式(即所有帧排列到一张图片中)。所以我的引擎处理的最小对象是 Texture,它从一张静态图片中切割出帧并储存宽高,并且可以从对应文本文件中加载宽高、分割帧信息。

但这样的抽象程度还不够。

考虑到图片中的帧不一定对应实际游戏中的帧,比如一个8帧的行走动画用0.5秒播放完比较合适,也就是实际使用 30 帧播放,我们可以得出进一步的抽象应该含有一个 Texture 实际播放的时间。我认为这一层可以将游戏中的所有元素视作 一个特定的 Animation,拥有屏幕坐标,持续时间,循环类型(显然有些动画需要只播放一次,有些动画需要无限循环)。

接着,我们想到游戏中的人物死亡时应该有死亡动画,比如颜色变深红之后逐渐消失。或者有些动画需要亮度闪烁。所以我们引入 Effect,储存关于 RGBA 通道系数的数组,然后进行线性插值,渲染时把对应通道值乘上系数。这样就可以实现动画的颜色+透明度变换动画。

虽然我要写的是 2D 游戏,但实际上也需要实现 z 轴的遮挡关系。比如两个人物一上一下靠的很接近时,人物会出现重叠部分,我们需要让站的靠下的人显示在上方。但我们又不必要完全实现z轴,所以我把动画分为几层:

  1. 地图地板
  2. 人物
  3. 子弹
  4. 爆炸特效
  5. UI

每一层实现一个动画链表,把不同的动画插入对应的层中。然后渲染时自底向上渲染,就自然实现了遮挡的效果。

Dungeon Rush Pipeline

此时渲染引擎基本写完。

我认为我这次做的最有挑战性的工作是根据 SDL 的有限功能实现一个功能完整、易于使用的渲染引擎。前期的良好架构为我后期的开发提供了很多便利。

实际上,我用了一个月构思大的架构,半个月思考并实现渲染引擎。最后,从只有测试接口的渲染引擎,到可以游玩的游戏,我只用了2天。整个游戏逻辑开发只用了半周。

之后的工作只不过是寻找动画资源、增加武器和怪物数据。而接口都是写好的,每一个怪物和武器的设置不过对应几行代码。UI 界面也是直接调用渲染引擎,使用游戏中的墙壁和地板元素拼出。