Turmoil

A slow learner.

Dungeon 金牌收录

一、项目简介

之前很喜欢《元气骑士》这种风格的手机游戏,所以也想做一个类似的 Roguelike 游戏。刚好最近学习了一些基本的设计模式,就把这个项目当作是我的初步实践。此外,由于之前也写过一些游戏,但是每次都从零开始,对于游戏内对象的管理也比较混乱,所以这次也希望构建一个简单的通用游戏框架,使得游戏具有更强的灵活性与可扩展性。

更多内容以及可执行文件下载可以访问我的个人网站:Tony's Studio

2022/12/24 更新:添加了更多功能与细节,修复了许多导致崩溃的 BUG。

二、运行截图

battle-1

battle-2

三、项目源代码及编译说明

项目源代码:Gitee - Dungeon

该项目解决方案下包含三个工程:Dungeon,Dungine 和 TinyXML2。其中 TinyXML2 工程是为了把 TinyXML2 库打包成静态链接库方便使用,编译时直接编译整个解决方案即可。Release 模式下,编译成功后可执行文件将输出到 Publish\ 目录下;Debug 模式下,编译成功后可执行文件将输出到 Build\dist\Debug\ 目录下。默认采用 Release 模式编译,程序中有关调试信息的宏已关闭。

编译环境如下:

  • Windows 11 Pro
  • Visual Studio 2022 Community
  • EasyX 20220901
  • FMOD 0.2.2.7

注:EasyX 20220901 中将消息类型前缀 EM 改为了 EX,可以在代码中引用 graphics.h 头文件提供对旧版本 EasyX 的兼容。

四、项目实现

该项目包含游戏框架部分 Dungine (Dungeon Engine) 和游戏主体 Dungeon 两部分。除了 EasyX 外,还使用了音频库 FMOD,以及用于 XML 解析的 TinyXML2。

4.1 Dungine

该部分是一个较为通用的游戏框架,包括游戏中基本类型的定义,以及设备相关的封装,同时也包括一个简易的 UI 库。

4.1.1 游戏对象

框架的最核心部分之一是对游戏对象的抽象。对于游戏中需要的常见对象,比如角色、武器等,均使用了工厂模式和原型模式进行创建,并通过组件模式添加各种行为和属性。下面展示了游戏对象类和组件类的基本声明,项目中的具体实现要稍复杂一些。GameObject 有一个重要的成员 m_isValid,因为删除对象并不是直接进行的,而是通过设置该标记,然后由场景类删除。这里的 AbstractObject 是更一般的对象,包括对组件等的抽象,其提供了原型模式的两个 Clone 方法。

class GameObject : public AbstractObject
{
public:
	GameObject(Scene* scene);
	virtual ~GameObject();

	// 子类调用该方法生成新的子类。
	virtual GameObject* Clone() const;
	// 子类复制时调用父类的该方法实现父类中成员的复制。
	virtual void Clone(GameObject* clone) const;

	// 游戏对象的更新和绘制。
	virtual void Update(Event* evnt);
	virtual void Draw();

	// 组件的添加与获取。
	void AddComponent(AbstractComponent* cmpt);
	template<typename T> T* GetComponent()
	{
		auto it = m_components.find(T::StaticName());
		if (it != m_components.end())
			return static_cast<T*>(it->second);
		else
			return nullptr;
	}

protected:
	Scene* m_pScene;	// 游戏对象受所在场景的管理。

	// 所有组件以及按更新顺序排列后的组件。
	std::unordered_map<const char*, AbstractComponent*> m_components;
	std::multimap<int, AbstractComponent*> m_cmptUpdateQueue;

	bool m_isValid;
};


class AbstractComponent : public AbstractObject
{
public:
	AbstractComponent(GameObject* gameObject, int updateOrder);
	virtual ~AbstractComponent() {}

	static const char* StaticName();
	virtual const char* Name() { return StaticName(); }

	virtual AbstractComponent* Clone() const;
	virtual void Clone(AbstractComponent* clone) const;

	virtual void Update(Event* evnt);

protected:
	GameObject* m_pGameObject;	// 组件必须获得其所属的对象以便更新。
	int m_updateOrder;
};

场景类对游戏中的所有对象进行管理,主要通过对象池实现。(这里的对象池只是保存游戏场景中的所有对象,并不是提供对象复用的功能。)所有在更新过程中遇到的对象添加、移除等动作都会在更新完成后统一处理。

class Scene
{
public:
	Scene();
	virtual ~Scene();

	virtual void Update();
	virtual void Draw();

	void AddObject(GameObject* object);
	void RemoveObject(GameObject* object);

protected:
	void _DeleteObject(GameObject* object);
	void _UpdateObjectPool();

	// 当前需要更新的所有对象。
	ObjectPool m_gameObjects;
	// 更新中添加的对象会先保存至此。
	ObjectPool m_pendingObjects;
	// 更新中删除的对象会先保存至此。
	ObjectPool m_dirtyObjects;

	bool m_isUpdating;
};

class ObjectPool
{
public:
	ObjectPool();
	~ObjectPool();

	void Update(Event* evnt);
	void Draw();
	
	void AddObject(GameObject* object);
	bool RemoveObject(GameObject* object);
	bool DeleteObject(GameObject* object);
	
	void Clear();	// 清空对象,但不进行 delete。
	void Destroy();	// 释放并清空所有对象。

private:
	std::vector<GameObject*> m_pool;
};

4.1.2 图像绘制与音频播放

对于图像绘制,这里把 IMAGE 类封装为了 Symbol,包含位置、图层、旋转角度、缩放比例、透明度等信息。由 Device 类管理绘制,使用画家算法,对所有 Symbol 排序后进行绘制。

对于音频播放,这里封装了 FMOD 的相关函数。声音分为两种,一种是短时间的音效,比如按钮按下的声音;一种是长时间的背景音乐。

4.1.3 资源管理

这里按我自己的想法实现了一个资源管理器,在程序开始运行时,仅从外部 XML 文件读取所有资源的索引。当需要某一资源时再通过索引加载,并在资源不被使用时自动将其释放。资源分为图像资源、音频资源、动作资源三种,其中动作资源是提供给动画使用的 Sprite Sheet。

4.1.4 UI 库

上一个版本的 UI 库参考了这篇文章:EasyUI:基于 EasyX 的 UI 界面库(by 祝融)。在这一版本中,我根据我的需求对其进行了重构,包括实现细节与各种类的组织关系,同时添加了如许多新特性。所有页面都被封装成类,并被页面管理器(Application)管理,并并由其启动。页面支持切换的过渡动画以及子页面(弹窗)的实现。

UI 控件均可通过外部 XML 文件加载,支持绝对坐标和相对坐标,并可根据屏幕大小自行适配。同时,还可以添加动画效果,不过该功能目前并不完善,只能支持简单的位移、缩放和透明度变化效果。

此外,键盘鼠标信息的接收也包括在 UI 库中,这里仅仅使用数组记录按键信息,不过将按键信息分为两种:InstantKey 是只要按下就是 true,松开就是 false;SluggishKey 则是只有按下的第一帧是 true,之后若不松开,也会变为 false。这里额外检测了窗口激活消息,如果窗口失去焦点,则会停止接收键盘和鼠标消息。

4.1.5 其他

除了主要功能外,该框架还提供了一些其他的功能,比如基于 TinyXML2 的 XML 解析,向量运算,四叉树(参考自四叉树碰撞优化)等,还提供了如单例模式、原型模式、工厂模式等的模板基类。这里在工厂模式的基础上设计了 Library 类,用于存放一类对象的所有原型。其中的原型均通过工厂模式从 XML 文件创建,此后该类对象便可直接从 Library 中的原型直接复制得到。

4.2 Dungeon

该部分是游戏的具体实现,包括游戏核心流程,地图的生成,游戏对象的具体实现,以及游戏的各个页面。

4.2.1 核心流程

游戏核心流程由 Dungeon 类实现,其派生于 Scene 类。由于所有游戏对象的更新和绘制均可由对象池统一管理,因此其主要进行资源的初始化以及调用地形的生成,还有一些特殊对象,如随机宝箱(Crate)的生成等。这里使用了四叉树进行碰撞优化。

4.2.2 地图生成

地图的生成基于一个 3 * 3 的网格图,通过随机化 Prim 算法生成一个顶点数为 3 ~ 9 的树作为地图的基本形状,顶点数与当前关卡数和游戏难度成正相关。树的顶点随后生成房间(Arena),边生成连接房间的桥(Bridge)。房间中障碍的生成有三种模式,也可能无障碍。每个房间内含 Graph 类,Graph 类将房间划分为网格,记录障碍信息,并提供 A* 寻路算法和寻找空白区域算法的接口。

4.2.3 对象行为

所有对象均通过组件赋予其属性或行为。属性方面,比如,参与碰撞的物体都有 RigidBodyComponent 和 ColliderBoxComponent 组件,移动的对象都有 MoveComponent,需要绘制的对象都有 AnimComponent 等。行为方面,有行为的对象均包含 BehaviorComponent,而对象的每一个行为都是一个派生自 Behavior 的类,而非通过 if - else,这样使得对象的行为有更大的灵活性,也可以方便地添加更多行为。类似的,对象的状态也是如此。下面是行为组件的大致实现。

class BehaviorComponent : public AbstractComponent
{
public:
	BehaviorComponent(int updateOrder);
	virtual ~BehaviorComponent();

	virtual const char* Name();

	virtual BehaviorComponent* Clone() const;
	virtual void Clone(BehaviorComponent* clone) const;

	virtual void Update(Event* evnt);

	void AddBehavior(Behavior* behavior);
	void ChangeBehavior(const char* name);

private:
	std::unordered_map<const char*, Behavior*> m_behaviors;
	Behavior* m_pCurBehavior;
};

class Behavior : public AbstractObject
{
public:
	Behavior();
	virtual ~Behavior() {}

	virtual const char* Name() const;

	virtual Behavior* Clone() const;
	virtual void Clone(Behavior* clone) const;

	virtual void Update(Event* evnt);

	virtual void OnEnter();
	virtual void OnExit();

protected:
	BehaviorComponent* m_parent;
};

4.2.4 游戏页面

游戏包括主页面、设置页面、关于页面等,每个页面的 UI 控件样式及布局均由外部 XML 文件提供,但是事件的绑定还是在程序中进行。游戏界面还需要管理 Dungeon 类的初始化和每帧的更新,同时游戏内的部分元素,比如玩家的状态栏、关卡数提示等,均由 UI 控件实现,因此也需要一定的交互。

4.2.5 可扩展性

游戏中绝大部分数据均从外部 XML 文件加载,因此,如果不涉及对象行为逻辑的更新,可以在不进行重新编译的情况下对游戏内容进行较大幅度的改动。比如角色、武器、子弹等属性的调整,新武器、新敌人、甚至新地图样式的添加都可以完成。

五、总结

经过半年多空闲时间的零碎更新,实现了最初构想的所有功能需求,同时在原先的基础上丰富了许多内容,包括资源文件以及代码细节上的丰富,不过代码整体架构没有太大改动,依然比较简陋。希望作为一个入门练习能够抛砖引玉,以后做出更好的作品。

评论 (13) -

  • ------------------------------
    03/15/2023 00:39:03
    Error: No file linked to Pipe
    Error: Failed to load "Settings"
    Error: Cannot load "res\data\Resource.xml" - Error code: 3
    Error: Failed to initialize Device

    楼主这种情况应该是发生什么错误了?
    • 为了减少体积与方便更改,源代码中移除了资源文件。因此如果要直接编译运行的话,需要手动将资源文件拷贝至工程目录。具体方式在 Gitee 和 GitHub 的 README 里有提到。
  • 求大佬求解,为什么我用VS2022编译时出现 无法打开lib/fmod_vc.lib文件
    • 可能是你没有编译整个解决方案。在编译 Dungine 结束后,会自动拷贝该文件至对应目录的。
  • 大佬,这是需要额外安装什么东西吗,我用VS2022打开后编译怎么报错了呢?
    • 应该是 EasyX 更新后把消息里的 EM 前缀改成 EX 前缀了,在 Dungine 的 Event.cpp 里改一下就好。

添加评论