游戏引擎学习第226天
引言,计划
我们目前的目标是开始构建“元游戏”结构。所谓元游戏,指的是不直接属于核心玩法本身,但又是游戏体验不可或缺的一部分,比如主菜单、标题画面、存档选择、选项设置、过场动画等。我们正在慢慢将这些系统结构搭建起来。
我们设计主要是根据我们自己对游戏体验的偏好来构思的,并不遵循某种固定的游戏设计理念。整体方向是减少繁杂的菜单和界面操作,希望尽量多地将所有体验都放在游戏世界本身中。我们更喜欢游戏在启动后能尽快进入实际的内容,而不是停留在冗长的前置菜单或流程中。因此,我们会尽可能避免设置复杂的前置界面,把主要体验都集中到游戏内部。
此外,在设计这些系统时,我们也会有意识地构造一些能够带来挑战性的编程任务。我们希望通过这些例子,展示在开发中如何解决实际问题,以及如何编写接近专业水准的高质量代码。因此,这个过程不仅是开发一款游戏,也是在演示和教学,展示如何系统地构建一个稳定、清晰、模块化的游戏架构。
总之,我们将尽量让游戏结构简洁直观,体验一体化,让玩家尽快进入游戏核心内容的同时,也为我们提供展示编程思维的机会。
介绍所需的“元游戏”元素
我们不打算为游戏加入太多的元游戏结构,但至少需要两个元游戏部分,以营造我们想要的整体氛围。
第一个是游戏启动时能够播放一个开场动画或序列。原因是我们希望在玩家正式进入游戏前,提供一点点背景和情境,但又不希望这些情节内容嵌套在游戏本体内。因为是一款节奏快速、以直接行动为主导的游戏,我们不希望其中出现太多的中断或过渡内容来影响流程。因此,我们更偏好一种传统街机风格的吸引模式(attract mode):当玩家启动游戏却没有进行任何操作时,系统会自动播放故事片段或者动画,营造氛围。这些内容会循环播放,直到玩家正式开始游戏。
我们已经为此实现了过场动画(cutscenes),就是用于这种吸引模式的内容。理想的流程是:玩家打开游戏,看到一段循环播放的剧情片段,等到玩家按下某个按钮时,才进入游戏主体验,不需要经过其他干扰流程。
第二个部分就是游戏主模式本身。一旦玩家选择开始,就会进入实际的游戏世界开始游玩。因此,游戏将只包含两个主要模式:吸引模式 和 游戏模式。
我们希望整个体验尽可能简洁直接。我们不打算添加“选择存档”、“调整选项”这些常见的菜单内容。比如音乐开关、操作设置等选项,我们希望能以更有趣的方式整合进游戏本体中,比如让玩家控制角色跳到某个开关上来设置音量,而不是通过弹出的菜单进行设置。
虽然我们只计划实现这两个模式,但它已经涵盖了解决元游戏结构的大部分编程问题。核心在于让游戏具备“理解模式”的能力,知道当前处于哪个状态,以及如何从一个模式切换到另一个。无论之后想添加什么功能,比如存档选择、选项菜单、联网匹配等,本质上只是扩展了更多模式而已,底层的处理逻辑是一样的。
因为我们不喜欢那些复杂的外部结构,所以我们选择跳过它们,尽量把所有元素都融入到游戏内部。如果日后发现确实需要某些设置功能,我们也会努力用游戏化的方式整合进主体验中。
设置、编译并运行当前游戏状态
我们之前刚完成了淡入淡出的功能设置,但回头看,发现有一个问题我们还没有解决,那就是淡入淡出效果的非线性表现。换句话说,明明我们设置的是线性变化的淡入淡出值,但屏幕上的变化却看起来不是线性的,可能是变化太快或太慢,曲线感不自然。
这让我们怀疑在颜色处理方面可能存在一些问题,比如颜色值是不是没有经过正确的 gamma 校正(伽马矫正)。也就是说,也许我们对颜色的线性空间和 sRGB 空间之间的转换处理有误,这可能导致我们乘以 alpha 值的时候产生了不符合预期的结果。虽然目前还不确定具体问题出在哪,但可以确定淡入淡出的亮度变化看起来确实不对劲。这件事虽然暂时不打算处理,但未来肯定是需要认真排查的。
然后说回游戏的当前状态——目前运行时的逻辑大致是这样:
- 游戏启动后,会播放过场动画(cutscene)。
- 按下空格键后,游戏会切换到主游戏模式,主角(hero)出现,进入游戏场景。
- 如果在游戏中按下 ESC 键,则会返回到 cutscene 画面,形成一个可以来回切换的流程。
- 每次重新进入游戏时,会重新创建一个新的玩家角色,这部分逻辑目前还没完善,也就是说玩家的数据、状态并没有被正确保存或复用。
接下来我们主要要处理的就是这一块——从 cutscene 进入游戏后,角色的初始化、状态管理等流程还没有真正建立起来,这是我们首要需要补上的一环。我们希望把这个基本的“切换与状态保存”系统逐步搭建完善。
发现一个bug 第三场的图层太多
怎么可能有8个图层
回顾之前的一个问题
多次从游戏切换到场景动画,再切换回游戏会出现多个角色
选择今天的第一个编程任务
我们现在要处理的重点是:当我们从游戏主模式返回到“吸引模式”(attract mode,即播放剧情的模式)时,应该能够识别出这是一次“模式切换”。不能简单地回到我们上次离开剧情模式时的状态,因为那样会造成玩家退出游戏再回来时看到的是剧情中间的某一帧,这并不符合我们预期的体验。
我们真正希望的是,每次回到吸引模式时,都应该重新开始播放剧情,从头开始,而不是从中断的地方继续。这需要我们在程序中有一种机制,能够识别“从主游戏切换回剧情模式”这种事件,并据此进行适当的重置操作。
要实现这一点,我们就需要引入某种“模式切换检测”机制,来标记是否发生了从一种模式切换到另一种模式的事件。也就是说,程序不仅需要知道“当前处于哪种模式”,还要知道“是否刚刚从另一种模式切换过来”,以便在必要时执行初始化流程。
比如,从主游戏切回剧情时,我们可以重置剧情状态、重新开始播放,而不是继续旧的状态。这也意味着我们的模式管理逻辑需要更智能,不仅要维护当前模式,还要对每次模式切换进行处理,确保进入新模式时能正确初始化所需内容。
目前这就是我们接下来要解决的核心问题,也是构建一个基础“元游戏(meta game)系统”的关键一环。后面会逐步完善每个模式切换时该做什么、如何重置状态、如何让体验自然连贯。
审查过场动画代码
我们目前的系统存在一个比较基础的问题,那就是:游戏并没有明确知道“什么时候切换了模式”。我们只是通过非常简单的方式来决定是在播放剧情模式还是进入了游戏——判断当前是否存在任何一个英雄角色(Hero)。只要存在一个英雄,我们就进入游戏模式;当一个也不存在时,我们就自动回到剧情模式。
但是这样做带来一个问题:游戏无法明确知道“切换模式”这件事本身发生了。比如,我们没有任何机制能告诉系统“现在你刚从游戏切换回剧情模式”,这就意味着无法触发某些“重置行为”,比如重新播放剧情。剧情播放就会从上次退出时的位置继续,这不符合预期。
为了解决这个问题,我们需要建立一个更正式的“模式管理系统”。也就是说,要有一个明确的变量或状态来记录游戏当前处于哪种模式,同时也能记录“模式之间的切换”,以便我们能在切换时做出适当反应(例如重置剧情)。
我们当前有一个 game_state
,其中有一个 playing_cutscene
,这个结构可以控制剧情播放。但我们并没有一个明确的“吸引模式状态结构”。接下来我们要做的,就是为“吸引模式(Attract Mode)”引入一个新的状态结构,比如叫 attract_mode_state
,它可以包含以下内容:
- 当前的
cutscene
对象:用于管理剧情的播放; - 可选的标题画面状态(title screen):比如“Press Start to Play”这类提示;
- 当前处于哪个子状态(比如是在标题画面阶段,还是剧情播放阶段);
- 可以设置每个子状态的时间控制,比如标题画面停留时间、剧情自动循环时间等;
对于子状态的控制,我们可以通过一个简单的枚举变量来管理,比如:
enum attract_sub_state {Attract_TitleScreen,Attract_Cutscene
};
这样,我们就可以让“吸引模式”本身也具有状态切换功能。整个流程可以是这样:
- 启动游戏,进入
AttractMode
; - 显示标题画面几秒钟;
- 自动切换到剧情播放;
- 剧情播放完或达到某个条件(比如超时),重新切回标题画面;
- 玩家按下开始键(空格键等),切换到
GameMode
; - 玩家按下退出键(ESC),重新切回
AttractMode
; - 此时识别到是一次模式切换,于是重置剧情状态,让其从头开始。
通过这种方式,我们不仅能清晰管理游戏当前的运行状态,还能实现真正“响应式”的模式切换行为——每次切换都能执行预定义的初始化逻辑。
总结来说,我们的下一步工作目标就是:
- 引入
AttractModeState
; - 定义吸引模式下的子状态(标题画面、剧情播放等);
- 实现统一的模式管理逻辑,明确记录和响应每次模式的切换;
- 让切换逻辑驱动内容重置,而不是依赖底层状态变化(例如英雄是否存在)这种隐式行为。
这将为整个“元游戏系统”的扩展打下一个更坚实的基础。
添加游戏模式的概念
我们现在要为整个游戏的模式管理系统建立一个更清晰、结构化的框架。具体来说,我们决定定义一个统一的“游戏模式(game mode)”状态变量,用来描述当前游戏处于哪一个大阶段。这个状态可以是以下几种之一:
- 标题画面(Title Screen):初始展示界面,比如“按任意键开始”等提示;
- 剧情播放(Cutscene):播放故事内容;
- 实际游戏世界(Playing / World):玩家控制角色在游戏中活动的主要阶段。
除了这三种基本模式之外,我们还预留了扩展可能,比如:
- 选项模式(Options):如果未来决定实现类似设置选项的系统;
- 存档模式(Save Slots):如果需要支持多个存档位,也许会在游戏中通过场景来实现,例如“孤儿院中不同房间代表不同存档”,通过角色在房间中移动来选择存档,而不是弹出传统菜单。这种设计本质上是“菜单在游戏中实现”的一种思路,虽然它是交互式的,但也可视为一种游戏模式。
接下来,我们要为每一种模式建立独立的数据存储结构。例如:
- 标题模式需要记录的是当前是否显示提示文字、倒计时是否完成等;
- 剧情模式需要管理剧情的播放进度;
- 游戏模式需要完整的世界状态,比如角色、地图、敌人、道具等。
这些模式状态是互斥的,也就是说,游戏任何时刻只会处于其中一种模式。一旦切换模式,前一种模式的状态数据就可以安全地被清除或重置,不必一直保留。
这也启发了我们下一步的架构设计思路:
- 使用一个枚举类型
GameMode
,统一标识当前所处模式(例如:GameMode_TitleScreen
,GameMode_Cutscene
,GameMode_Playing
); - 创建一个统一的结构体
GameState
来包含当前的游戏模式字段; - 为每种模式编写专属的状态数据结构(例如:
TitleScreenState
、CutsceneState
、WorldState
); - 当游戏模式切换时,负责的系统将旧模式状态清除,并初始化新模式状态;
- 每一帧游戏更新时,主循环会根据当前
GameMode
决定调用哪一个子模块的更新和渲染逻辑; - 模式之间的切换逻辑要有明确的触发机制(例如:用户按下空格键、ESC键,或者剧情播放完毕等)。
这样做的好处是:
- 各种逻辑清晰分离,便于维护;
- 模式切换变得显式而非隐式(不再依赖是否存在角色等间接逻辑);
- 更容易扩展新的功能或模式,不会影响已有逻辑;
- 各模块状态独立,避免状态混淆。
总结来说,我们当前的目标是在程序架构中正式引入“游戏模式”这一核心概念,并为每种模式分配对应的状态管理系统,使整个游戏运行逻辑更加清晰、稳定和可拓展。
向game_state
中添加一个联合体,用于存储每个游戏模式的独立状态
我们现在正在继续构建并优化游戏的整体运行架构,重点在于把不同游戏阶段的状态拆分成更清晰的结构,提升可维护性和灵活性。
我们已经确定引入“游戏模式”这一核心概念,并且将当前的游戏状态划分为几种主要模式,例如吸引模式(Attract Mode)、游戏世界模式(World Mode)等。每种模式都有对应的状态结构,比如:
- 吸引模式状态(Attract Mode State):用于管理如标题画面和剧情播放等状态;
- 游戏世界状态(World State):用于记录玩家游戏进度、地图状态、角色等游戏内部细节。
目前已经存在部分结构如 world
,可以用于承载游戏世界的状态。我们不预先假设状态结构具体要怎样构建,而是先搭出基本框架,逐步从现有代码中提取相关内容,灵活调整架构。
此外,为了管理内存使用,我们决定采用动态分配(allocation-style)的方式来管理这些状态结构,尤其是考虑到某些模式的状态数据可能会很大,这样做便于在模式切换时彻底释放无用资源,提高运行效率。当然如果以后发现状态体积较小,也可以退而采用常驻结构。
当前正在着手实现的具体内容包括:
-
在主状态结构中创建两个子状态指针或对象:
AttractModeState* attractMode;
WorldState* worldState;
-
接着分别实现标题画面和剧情的更新与渲染函数:
UpdateAndRender_TitleScreen()
UpdateAndRender_Cutscene()
-
游戏主循环将会根据当前游戏模式(
gameMode
)的值来决定调用哪个更新渲染函数。我们准备用switch-case
结构来替代旧的逻辑判断,切换明确、代码清晰。 -
接下来会将旧有的“是否存在角色”来决定状态的逻辑废弃掉,转而使用显式的模式变量
gameMode
来驱动游戏行为。
通过这种方式,所有的游戏状态变更都会变得更加明确、易于追踪。例如,当切换回“吸引模式”时,可以显式地初始化对应状态,并从头开始播放剧情,而不是停留在上次中断的位置。这样能保证游戏体验更连贯,也为后续添加更多模式(如选项、存档、多人等)预留了空间。
此外,在调试时注意到一个界面显示的小细节——当前活跃行高亮显示为蓝色,虽然这一直如此,但今天才突然意识到它其实是一个正常行为,而不是异常,也记录下来避免未来误判为渲染问题。
总结起来,我们正在对整个游戏逻辑流程进行系统性拆解与重构,通过状态模块化、显式模式管理、动态内存控制,让代码结构更清晰,运行效率更高,并为扩展性做好铺垫。
更改UpdateAndRenderGame
函数,根据当前游戏模式调用不同的函数
我们目前正在实现游戏主循环中的核心逻辑 —— 根据游戏模式(Game Mode)切换具体的更新与渲染函数,从而管理不同阶段的游戏行为。我们已经定义了几种明确的游戏模式,并使用 switch
语句来调度对应的更新与渲染函数。
目前支持的三种模式包括:
- 标题界面(Title Screen)
- 过场动画(Cutscene)
- 游戏世界(World)
我们在代码中引入了 GameMode
枚举或类似的机制,根据当前游戏状态(gameMode
)来决定调用哪一个函数。每个函数负责自己那一部分的逻辑处理,比如:
UpdateAndRender_TitleScreen()
:负责标题画面的更新和绘制;UpdateAndRender_Cutscene()
:处理过场动画的演出逻辑;UpdateAndRender_World()
(原本可能叫UpdateAndRender_Game
,但为更清晰重命名):处理游戏世界的实时更新与渲染。
为了防止意外的模式值进入,我们在 switch
的默认分支中加入了 InvalidCodePath()
,用于捕捉非法状态,这是一种非常有用的错误检测手段。同时也可以预留一个 GameMode_None
或 GameMode_Uninitialized
的默认值,表示游戏尚未进入任何已定义状态,尽管目前暂时未启用。
在结构上,我们为每种模式准备了独立的状态存储:
GameMode_Attract
:可能是一个空壳,暂时未定义明确内容,作为吸引模式或默认入口;GameMode_Cutscene
:已有明确结构,用于管理当前的过场动画状态;GameMode_World
:世界状态,承载玩家进度、地图、实体等游戏运行核心数据。
每当进入某个模式时,系统会调用对应的 UpdateAndRender
函数,并将其状态结构(如 cutsceneState
、worldState
等)传入,从而实现模块化的逻辑分离。
我们也在思考这些函数的参数结构,比如:
- 必须传入
dt
(每帧时间差); - 需要完整的
Input
信息,用于交互响应; - 某些特殊逻辑(例如“当前过场动画是什么”)可能需要引用模式状态中的特定字段,而不是全局变量。
这就引出一个更清晰的状态架构设计:每种 GameMode 拥有自己的状态数据结构,所有更新与渲染函数只接收并操作自己的那一份数据。这种方式更具封装性,也利于状态重置和清理。
总结当前的实现重点:
- 使用
GameMode
来控制游戏运行状态; - 利用
switch
实现清晰的状态调度; - 为每种模式维护独立的状态结构;
- 使用
UpdateAndRender_*
方式进行各模式逻辑隔离; - 将
dt
和输入作为主要参数传递; - 为潜在的非法状态增加安全检测逻辑。
这一套架构为后续的扩展(例如设置菜单、存档界面、多存档入口等)提供了良好的基础,使整个游戏状态管理系统更有条理、可控且易于维护。
修改过场动画代码,使用新的游戏模式概念
我们现在正在调整和完善“过场动画(cutscene)”在游戏状态系统中的初始化与使用方式,具体做法是通过外部传入的内存分配区域(arena)来管理其生命周期。这种方式的核心思路是:由外部逻辑分配一个 arena,传入到初始化函数中,让该函数负责在这个 arena 内构造其内部状态对象,并返回结果。
初始化方式变更与状态构造
我们决定不再由内部代码自己决定如何分配内存,而是由调用者提供一个 arena。我们在 arena 上执行 PushStruct
,为“正在播放的 cutscene”分配状态结构,完成初始化后直接返回。这样就避免了显式的销毁逻辑,因为只需在适当时机整个 arena 被丢弃(jettison),就可以整体释放相关资源,非常适合这种临时状态用途。
- 实例函数:
MakeIntroCutscene(arena)
- 由外部传入 arena。
- 在 arena 中创建 cutscene 的状态数据结构。
- 初始化完毕后返回。
由于我们采用了 arena 的分配方式,因此暂时不实现复杂的 shutdown 或 teardown 操作。大多数时候,我们只需丢弃 arena 即可释放全部资源,简单高效。
更新与渲染逻辑的统一入口
在运行时,更新与渲染 cutscene 的逻辑将通过统一入口完成:
UpdateAndRender_Cutscene(GameMode_Cutscene*)
- 传入的 cutscene 状态结构指针来自初始化时构造。
- 当前处理较简单,不做复杂处理,只做最基本的状态推进与绘制。
如果未来需要支持多个 cutscene 同时存在(目前假设只有一个在播放),架构也允许我们扩展,例如通过注册机制或状态栈来实现更复杂的逻辑。
Title Screen 与其他 GameMode 的一致管理
UpdateAndRender_TitleScreen
的结构会类似,目前尚未确定它具体的内部数据结构。我们可以为 title screen 定义一个独立的 GameMode_TitleScreen
状态结构,用 arena 分配管理,便于统一管理逻辑。
目前已确定的 GameMode 类型有:
GameMode_Attract
GameMode_Cutscene
GameMode_World
GameMode_TitleScreen
(即将定义)
所有这些 GameMode 都将遵循统一结构,每个模式都有对应的状态结构体和渲染逻辑。我们会将这些结构实例(如 GameMode_Cutscene
)存入全局或 Game 状态中,确保在运行中可以被访问。
小问题修正与结构完善
我们还修正了一些小问题,比如指针类型声明错误、拼写问题等。随后我们确保所有的 GameMode 实例都正确存储,并能从主循环中被访问和调用。
目前的结构清晰如下:
- 每种 GameMode 都有自己的状态结构(存储在 arena 中);
- 使用
switch(gameMode)
分派到不同的UpdateAndRender_*
函数; - 初始化函数(如
MakeIntroCutscene
)由外部提供 arena 来分配状态; - 不再需要手动销毁状态,arena 生命周期统一管理;
- 可以轻松扩展更多 GameMode(如 Settings、Pause 等)而保持架构不变。
整体上,这是对游戏状态管理方式的一次结构升级,使状态分离更清晰,资源管理更统一,代码更具扩展性和可维护性。后续我们只需逐步补充各个状态的数据结构和渲染细节,就能搭建起完整的游戏主循环架构。
开始实现游戏模式之间的切换
我们现在正在正式规范“切换游戏模式(Game Mode)”的机制,目标是让状态切换变得更加清晰、可靠和易于维护。整个过程主要围绕着“提取初始化逻辑”并构建“统一切换入口”展开。
问题背景
当前游戏在初始化时会自动生成游戏世界,但这是不合理的:
- 一开始玩家并未真正开始游戏,因此不应该直接初始化游戏世界;
- 玩家可能中途退出当前游戏世界并重新开始一个新游戏;
- 当前模式下没有机制来“重新创建”或“销毁”旧游戏状态,缺乏灵活性。
因此,需要对 游戏模式切换的流程进行模块化处理,将初始化逻辑从主流程中剥离出来,构建统一的模式管理系统。
设计方案:引入 SetGameMode 函数
我们引入一个新的函数:SetGameMode
,专门用于设置和切换当前游戏所处的模式。
SetGameMode(GameState* gameState, GameMode mode)
该函数主要职责包括:
- 清理当前游戏模式的状态数据(比如释放 arena 或清空 arena);
- 根据传入的目标模式,初始化新的游戏状态数据结构;
- 设置
gameState->mode = mode
。
这一函数成为游戏运行过程中所有模式切换的唯一通道,具有良好的抽象性和拓展性。
内存管理策略:使用 Arena 分配
在每次切换游戏模式时:
- 使用 相同的 Arena 来清除旧模式数据并分配新模式数据;
- 当前暂时采用单个 Arena 的方式来简化逻辑;
- 未来可能引入双 Arena 结构(Ping-Pong)以避免交叉污染,便于异步预加载、过渡动画等更复杂的逻辑。
这种做法有两个好处:
- 切换模式时自动清除旧数据,无需显式销毁;
- 每种模式的状态结构都是在自己的内存区中独立维护,彼此不干扰。
整体架构
以下是 SetGameMode
的核心逻辑框架(伪代码):
void SetGameMode(GameState* gameState, GameMode newMode) {Clear(gameState->arena); // 清除旧模式所有状态switch (newMode) {case GameMode_TitleScreen:gameState->titleScreen = PushStruct(arena, GameMode_TitleScreen);InitTitleScreen(gameState->titleScreen);break;case GameMode_Cutscene:gameState->cutscene = MakeIntroCutscene(arena);break;case GameMode_World:gameState->world = InitGameWorld(arena);break;case GameMode_Attract:gameState->attract = InitAttractMode(arena);break;default:InvalidCodePath();}gameState->mode = newMode;
}
这样我们就能够:
- 在任意时刻切换到新的模式;
- 自动丢弃旧模式状态;
- 简化初始化逻辑,避免在主流程中混乱展开。
预期收益
- 结构清晰:每种模式的入口、状态、资源完全隔离;
- 易于扩展:添加新模式只需增加一个
case
和对应初始化函数; - 方便重启:可以轻松实现“重新开始”、“退出当前世界”等行为;
- 资源安全:通过 arena 生命周期统一管理内存,无需手动释放,避免内存泄漏或重复引用。
整体而言,这是对游戏运行状态管理系统的一次彻底升级,将切换逻辑标准化,使整体架构更加模块化、稳定,并为未来扩展打下坚实基础。接下来只需逐步补充具体模式的初始化与更新细节,就可以轻松搭建完整的状态控制系统。
将game_state
中的WorldArena
更改为ModeArena
,供所有游戏模式使用
我们正在进一步规范游戏模式的切换流程,并引入一种统一且高效的资源管理机制。这里的关键在于:
重新命名并重新定义 Arena 的用途
我们原本是将 Arena 用于“世界(World)”数据的内存分配,但这实际上已经不符合我们现在的设计需求。因为不同的游戏模式(例如标题界面、过场动画、游戏世界等)都需要自己的状态数据,而这些状态具有明显的“模式生命周期”特点。所以我们将 Arena 更名为:
Mode Arena(模式 Arena)
这代表着:所有与当前活动游戏模式有关的内存资源,都通过这个 Mode Arena 来管理。
清除 Mode Arena 的需求
由于游戏模式之间需要相互切换,因此我们需要有一种方式,在切换时清除当前模式所用的所有资源。
当前我们已有 BeginTemporaryMemory / EndTemporaryMemory 的机制,但这些是为了临时用途,不适合用于这种“模式级别”的清除逻辑。因此,我们决定引入一个新函数:
ClearArena()
这个函数的意义是:彻底清空 Arena 中所有已分配的数据,使其回到初始化状态。
本质上就是将 Arena 的内部指针(如 Used
)重置,使得后续可以重新开始分配。
ClearArena 的具体实现逻辑
Arena 的结构本身比较简单,主要包含:
- 分配起点指针(Base)
- 当前使用位置(Used)
- 总容量(Size)
所以清除它的操作只需把 Used 指针归零即可,或者简单地重新用同一 Base 进行初始化即可,像这样:
void ClearArena(MemoryArena* arena) {InitializeArena(arena, arena->Base, arena->Size);
}
这样一来,Arena 中所有此前分配的数据就都被视为“无效”,未来的模式就可以在这块内存上重新开始分配。
模式切换逻辑整理(SetGameMode)
完整的模式切换流程如下:
- 调用
ClearArena(gameState->modeArena)
清除旧模式的所有资源; - 使用
switch(gameMode)
根据目标模式类型进行分配初始化; - 将初始化好的模式状态结构赋值到
gameState
中; - 设置
gameState->currentMode = gameMode
。
具体伪代码如下:
void SetGameMode(GameState* gameState, GameMode newMode) {ClearArena(&gameState->modeArena);switch (newMode) {case GameMode_TitleScreen:gameState->titleScreen = PushStruct(&gameState->modeArena, GameMode_TitleScreen);InitTitleScreen(gameState->titleScreen);break;case GameMode_Cutscene:gameState->cutscene = MakeIntroCutscene(&gameState->modeArena);break;case GameMode_World:gameState->world = InitWorld(&gameState->modeArena);break;default:InvalidCodePath();}gameState->currentMode = newMode;
}
模式生命周期 = Arena 生命周期
通过这种方式,我们实现了非常干净的模式管理:
- 切换模式时,旧模式的状态自动被抛弃;
- 不需要手动释放资源;
- 每种模式都在独立的生命周期下运行,不干扰其他模式;
- 管理逻辑简洁、直观、稳定。
后续拓展
当前我们只支持三种模式:
- 标题界面(TitleScreen)
- 过场动画(Cutscene)
- 游戏世界(World)
将来如果添加新模式(例如设置界面、游戏结束界面、关卡选择等),只需:
- 增加一个
case
; - 编写对应的初始化函数;
- 分配状态结构到 Arena。
整个模式管理系统具备极好的拓展性和安全性,是构建复杂游戏状态机的坚实基础。
确定切换游戏模式时,如何传递参数
我们意识到,当前的 SetGameMode()
实现虽然可以完成游戏模式的切换,并负责根据不同模式初始化相关资源,但存在一个严重的设计问题:参数膨胀(Parameter Bloat)。
问题:不同模式初始化需要不同参数,导致参数管理混乱
以 GameMode_Cutscene
为例:
- 这个模式可能有多个具体的过场动画(如 intro、outro、boss fight 等)。
- 我们可能需要传入特定的参数,比如过场动画 ID、脚本数据、启动帧等。
但其他模式如 GameMode_TitleScreen
或 GameMode_World
并不需要这些参数。
这就导致
SetGameMode()
函数必须接受一个参数列表,这个列表是所有模式初始化可能需要的所有参数的合集。这种“参数大杂烩”方式非常臃肿、不清晰,也不利于后期维护。
解决方案:职责下放,将依赖数据的初始化交给调用者
我们重新设计了模式切换的流程,不再在 SetGameMode()
内部根据模式类型做 switch
和初始化。而是把这个初始化动作交由真正知道想要切换到哪个模式的调用者来处理。
于是我们引入了一个新函数:
BeginGameMode()
这个函数只做一件事:
- 清除现有的模式 Arena;
- 设置
GameState
的当前模式字段。
void BeginGameMode(GameState* gameState, GameMode newMode) {ClearArena(&gameState->modeArena);gameState->currentMode = newMode;
}
模式初始化交由调用者完成
调用者在清空 Arena 后,负责根据实际需求初始化对应的数据结构。例如:
BeginGameMode(&gameState, GameMode_Cutscene);
gameState->cutscene = InitCutscene(&gameState->modeArena, CUTSCENE_INTRO);
或者:
BeginGameMode(&gameState, GameMode_World);
gameState->world = InitWorld(&gameState->modeArena, selectedLevelID);
如果是标题界面:
BeginGameMode(&gameState, GameMode_TitleScreen);
gameState->titleScreen = InitTitleScreen(&gameState->modeArena);
优点总结
通过将初始化逻辑下放,我们获得了:
-
参数清晰明确
每种模式的初始化函数只接受它自己真正需要的参数。 -
更强的可维护性
新增模式时,无需修改中央SetGameMode()
函数,只需独立实现初始化逻辑。 -
职责分离明确
BeginGameMode()
只负责生命周期控制,初始化逻辑由调用方全权处理。 -
更好的扩展性
如果某个模式有多个配置变体(如不同类型的 Cutscene),调用者可以自定义行为。
总结一句话:
模式切换的资源管理与生命周期由
BeginGameMode()
控制,而模式内部的状态构建由调用者根据具体业务逻辑自行初始化,这种设计更清晰、灵活、健壮。
让SetGameMode
的调用者负责初始化新模式,避免传递不必要的参数
我们现在已经确立了一种更清晰的游戏模式切换机制,因此只需要根据这一逻辑,在具体的功能调用中使用这一机制就可以实现对应的效果。
基本流程
无论是播放引导过场动画(Intro Cutscene),还是进入标题界面(Title Screen),都遵循一个相同的结构化步骤:
-
切换游戏模式:
调用SetGameMode()
,传入目标模式(例如播放过场动画或显示标题界面)。- 这个过程会清除旧模式的 Arena。
- 将当前模式设置为新模式(如
GameMode_Cutscene
或GameMode_TitleScreen
)。
-
初始化该模式所需的结构体:
根据模式类型,构造新的GameModeCutscene
或GameModeTitleScreen
,并将其存入GameState
中的对应字段。
播放引导过场动画:PlayIntroCutscene
- 首先调用
SetGameMode(gameState, GameMode_Cutscene)
:- 清除旧 Arena。
- 设置当前模式为 Cutscene。
- 然后创建该 Cutscene 的状态:
gameState->cutscene = MakeIntroCutscene(&gameState->modeArena);
- 无需从函数返回任何值,直接就地完成创建并写入状态。
进入标题界面:PlayTitleScreen
- 同样的过程,调用:
SetGameMode(gameState, GameMode_TitleScreen);
- 然后初始化:
GameModeTitleScreen result = {}; // 或者更复杂的初始化逻辑 result.t = 0; // 似乎之前少了这一句,现在补上了 gameState->titleScreen = result;
Arena 相关修正
由于之前对 Arena 的管理进行了结构调整(引入了 modeArena
),需要同步修改 Arena 的初始化方式:
InitializeArena(&gameState->modeArena, base, size);
之前少传了 size
参数,现在补上。
整体总结
我们现在将**“游戏模式切换”**操作分成两个步骤:
-
清除旧状态 + 设置当前模式:
- 通过
SetGameMode()
统一完成; - 清除
modeArena
,避免内存泄漏或旧状态干扰; - 设定新模式标签。
- 通过
-
初始化新模式所需数据结构:
- 由调用者完成;
- 根据目标模式,自行决定初始化哪些数据,传入什么参数;
- 保持高度灵活和解耦。
通过这种设计,我们达到了以下目标:
- 清晰的内存生命周期管理;
- 简洁而清晰的模式切换机制;
- 模块间的解耦,利于扩展和维护;
- 避免冗余参数传递,提升代码可读性。
这是更合理、更工程化的一种做法,便于未来迭代开发与功能增加。
在World
结构体中创建一个专门供主游戏模式使用的arena,而不是使用game_state
中的arena
我们现在正在推进对 World Arena 的重构,目标是将其管理从 GameState
中剥离出来,使其直接归属于 World
。这是一种结构优化的方向,目标是增强数据的归属逻辑和结构清晰度。
当前设计的问题与目标
目前很多与世界相关的内容仍挂在 GameState
上,比如 WorldArena
。这种做法使 GameState
承担了过多具体模式的数据管理职责。而事实上:
GameState
应该是一个高层的容器,用来协调不同的游戏模式(如标题界面、过场动画、游戏世界等);- 每种模式的数据(比如游戏世界)应该自己管理自己的内存和状态;
- 因此,内存 Arena 的归属也应该归属于它们自身的结构体之中。
计划中的重构方向
我们希望让 WorldArena
不再是 GameState
的一部分,而是成为 World
结构体中的字段:
struct World {MemoryArena arena;// 其他世界相关的数据
};
这样的话,任何需要使用世界相关数据(包括内存分配)的时候,直接通过 world.arena
来访问,而不是从 gameState.worldArena
访问。
当前的暂缓处理
虽然最终我们希望把更多的状态迁移到 World
内部,但当前并不打算立即进行全部迁移。为了逐步过渡,现在的策略是:
- 在已有使用
worldArena
的地方,做一次简单的替换; - 将原本从
gameState.worldArena
读取的地方改成从gameState.world.arena
读取; - 暂时保留部分状态在
GameState
中,后续再视情况迁移。
实际替换处理
实际做法是查找所有使用 worldArena
的地方并修改为:
gameState.world.arena
或者在函数内部先获取:
World* world = &gameState->world;
MemoryArena* arena = &world->arena;
然后统一使用 arena
。
这样做的好处
- 结构清晰:
World
只管理自己相关的内存,符合内聚原则。 - 职责分离:
GameState
只关心各模式的切换与生命周期,细节下放。 - 后续可扩展性更强:多个世界、多段过场、多重模式并存时不易出错。
后续工作
虽然这次只替换了 worldArena
的归属,其他相关字段(比如世界逻辑状态、单位、地图等)后续也将按这种思路逐步迁移,最终目标是:
- 每个 GameMode 自己维护自己的状态;
- GameState 仅作为统一入口、调度与生命周期管理的顶层控制器。
这种设计更利于系统解耦、可测试性增强,也能提升代码的可维护性和长期扩展能力。我们会在接下来的开发中持续推进这种结构演化。
让基于世界的函数使用新的世界专用arena
我们正在对实体的添加与内存管理进行进一步的结构化,目标是把所有与“世界”(World)相关的内容明确归属于世界内部的一块专用内存区域(Arena),而不是像以前一样分散管理。这样做的意义和后续规划如下:
实体归属与内存结构
我们将像 add_low_entity
这样的函数所创建的实体,明确地划归到 world
内部的 Arena 中去。这是我们将所有世界级资源统一管理的第一步。
- 所有属于游戏世界(地图、实体、AI、路径等)的数据,都应该从
world
的 arena 中分配; - 这样将来在销毁世界或重新载入世界时,可以统一清除相关数据,无需担心遗漏或内存泄露;
- 这也使得非游戏世界的资源(比如 UI、标题界面、过场动画)能被独立管理,不互相污染。
模块分组的目的
我们之所以如此做,是为了逐步建立清晰的资源分组机制:
- 将整个游戏逻辑区分为几个大块,例如:
- 世界模块(World):包含所有游戏运行时的动态对象;
- 界面模块(UI):例如标题界面、选项菜单等;
- 系统模块:比如输入、声音、存档控制;
- 每一块逻辑都绑定各自的内存管理区域(arena),通过切换模式来完成激活与销毁;
- 这样不仅提高了系统的清晰度,也让调试和资源管理变得容易。
为什么要这样设计(即使当前项目不需要)
尽管当前的游戏项目(如一个小体量的游戏 demo)可能不需要如此严格的分离:
- 它只有简单的世界逻辑和一个标题界面;
- 没有复杂的设置菜单,也没有多重 UI 状态;
但我们依然选择构建这套架构,是为了 提前演示出一个更健壮和可扩展的架构模型,以便:
- 面对更大型的项目时无需重构;
- 其他人参考这套架构时也能适应复杂项目场景;
- 实现高复用性与最小耦合,利于多人协作开发。
下一步动作
我们正在实现一个叫 play_intro_cutscene
的过程:
- 它调用
set_game_mode
来清空当前模式、切换到“过场动画”模式; - 同时会在
modeArena
中初始化该模式所需要的数据; - 随着后续模块逐步加入,我们会持续将各种模式的初始化、更新、渲染流程模块化;
- 保持每个模式数据的独立性和结构清晰性。
这个架构虽然目前显得有些“超前”或“复杂”,但它为未来复杂游戏打下了良好基础,能够让系统具备应对更高需求的灵活性和健壮性。
完成在UpdateAndRenderGame
中的游戏模式切换语句
我们现在进入了关于渲染逻辑的重构阶段,重点在于将不同游戏模式下的渲染逻辑模块化,使每种模式都有自己独立、清晰的渲染流程。这一阶段的核心目标是:
标题界面的渲染逻辑整理
我们开始整理 render_title_screen
的调用和依赖:
- 这个函数需要接收一系列资源和上下文参数,包括:
assets
:素材资源;render_group
:当前帧的渲染组;draw_buffer
:绘制缓冲区;title_screen
:当前标题界面的数据结构。
这些参数都用于确保标题界面的更新与绘制能够顺利进行。通过传入结构清晰的参数,渲染逻辑可以专注于显示内容,不需要关心数据从哪来。
过场动画的渲染逻辑
随后我们检查了 update_and_render_cutscene
:
- 接收参数大体相似,但也略有不同:
- 除了
assets
、render_group
和draw_buffer
外; - 还需要
input_group
,因为过场动画可能包含用户交互(如跳过动画等); - 传入的数据结构是
cutscene
,而不是title_screen
。
- 除了
我们也注意到,在代码中总是容易拼错 cutscene
的大小写,因此需要更加注意书写规范。
内存区域管理
渲染函数内部还对 mode_arena
进行了检查:
- 这是当前游戏模式所使用的内存区域;
- 渲染逻辑依赖该 arena 中的数据结构;
- 保证了每种模式渲染时只使用自身对应的数据,避免状态混乱或数据污染。
整体整理的意义
这次对渲染接口的清理、参数规范化和内存区域确认,达成了几个目的:
- 模块清晰:不同游戏模式(如标题界面、过场动画)各自拥有独立的渲染逻辑;
- 依赖明确:每种模式需要的输入和输出结构清晰列出,方便后续维护;
- 内存安全:通过
mode_arena
保证数据生命周期受控,避免不必要的资源残留; - 统一接口:为将来进一步通用化或做状态切换提供了良好基础。
我们已经完成了标题界面和过场动画渲染函数的参数理顺和接入,接下来可以继续扩展到游戏世界本体的渲染,以及 UI、菜单等更多游戏状态的支持。这样整体框架将变得更加稳固和扩展友好。
为世界专用arena添加初始化代码
我们现在处理的问题是:在初始化游戏世界时,需要为其分配专属的内存区域(arena),以便其在运行期间可以自行进行内存分配,而不会污染其他游戏模式或全局状态。
核心改动与目标
我们决定对初始化流程进行重构,使得在调用 initialize_world
时,能够显式地传入要使用的内存 arena。这样可以让世界数据只使用分配给它的那一部分内存,保持独立和清晰。我们的主要目标包括:
- 初始化世界时,使用指定 arena;
- 该 arena 应从更大的父级 arena 中划出;
- 使用剩余内存大小作为子 arena 的大小;
- 初始化逻辑尽量模块化,不与
game_state
等全局结构强耦合。
具体步骤
-
更新
initialize_world
接口- 增加一个参数:传入用作分配用的
MemoryArena
; - 所有在世界中构造出来的数据都将使用这个 arena 分配。
- 增加一个参数:传入用作分配用的
-
子 Arena 的划分
- 获取父 arena 的剩余空间大小:
get_arena_size_remaining(parent_arena)
; - 使用
sub_arena
函数:通过剩余大小,从父 arena 中划出一个新的 arena 作为世界的 arena; - 该子 arena 就是世界在运行期间的唯一分配源。
- 获取父 arena 的剩余空间大小:
-
避免对
game_state
的直接依赖- 尽量不从
game_state
中直接引用全局变量; - 在调用初始化函数时主动传入需要的资源(如 arena),提高代码的模块化程度;
- 让函数对自身作用域内的资源负责,增强灵活性。
- 尽量不从
-
集成与替换
- 在游戏初始化流程中,用新的方式来初始化
mode_arena
; - 从而后续每个模式在构造自身内容时都有独立的 arena 可用;
initialize_arena
现可直接用于mode_arena
的初始化;- 世界数据就可以通过
initialize_world(mode_arena)
来构造。
- 在游戏初始化流程中,用新的方式来初始化
思路总结
通过这次重构,我们实现了世界数据的内存独立性,确保其内存分配只发生在自身的 arena 内。这样做的优点包括:
- 资源隔离:不同模式(如标题界面、世界、动画)不会共享 arena,避免数据互相干扰;
- 生命周期明确:每个模式内存生命周期由 arena 控制,清理操作也更方便;
- 扩展性提升:未来可以轻松支持更多模式和更复杂的状态切换,而无需担心内存管理问题;
- 逻辑清晰:初始化流程清晰明了,职责单一。
我们现在已经为 mode_arena
和 world_arena
建立了独立、可控的生命周期和作用域,为整个游戏结构的可维护性和健壮性奠定了重要基础。接下来可以继续清理、集成其他模式,推进游戏系统整体的结构化改造。
重新组织主世界游戏代码,将其放入新的世界游戏模式特定的函数中
我们当前的任务是进一步将“世界”的初始化逻辑独立出来,和其他游戏模式(比如片头动画、标题界面等)一样,进行模块化管理和切换。
目标
我们希望将一大堆原本零散存在的初始化代码集中整理起来,形成一个结构清晰、职责明确的“播放世界(PlayWorld)”函数,就像我们之前为“播放片头动画(PlayIntroCutscene)”所做的那样。这种做法不仅更清晰,也方便后续进行管理和扩展。
具体内容与操作步骤
-
识别与抽取世界初始化相关代码
- 当前代码中,有大量逻辑都是在创建世界时进行的,例如:
- 创建
WorldChunk
; - 初始化 world 中的 meters、坐标、数据结构等;
- 设置 audio state;
- 初始化 world arena;
- 创建
- 这些都跟 cutscene 完全无关,是纯粹属于“世界模式”的逻辑。
- 当前代码中,有大量逻辑都是在创建世界时进行的,例如:
-
建立
PlayWorld
函数- 类似
PlayIntroCutscene
的逻辑,创建一个PlayWorld
函数,集中完成:- arena 初始化;
- world 数据结构的构造;
- 设置
game_state.mode
为 world;
- 之后需要进入该模式时,只调用
PlayWorld
即可,所有相关数据都会自动设置完毕。
- 类似
-
整理代码位置
- 将和 world 有关的所有初始化代码全部拉出来,集中放在
PlayWorld
中; - 例如,
initialize_arena
,initialize_world
,set_game_mode
,game_state.world = result
等; - 原地只留下调用
PlayWorld
的代码,使主流程更干净简洁。
- 将和 world 有关的所有初始化代码全部拉出来,集中放在
-
临时处理不确定用途的数据
- 对于类似
ground_buffer_width
这类用途尚不明确的变量,先用TODO
标记; - 等后续明确其作用后再整理位置,暂时保留但不深入处理。
- 对于类似
-
结构进一步优化的方向
- 后续可以考虑将与世界无关的状态(如标题界面、片头动画)完全从
game_state
中移除,放入独立模块; world
内部使用world_arena
管理数据,与其他模式完全隔离;- 所有模式切换通过统一的
set_game_mode
和各自的PlayXXX
函数完成。
- 后续可以考虑将与世界无关的状态(如标题界面、片头动画)完全从
阶段性结果
我们目前已经完成了以下几点:
- 提取并封装了世界模式的初始化逻辑;
- 创建了
PlayWorld
,可以集中处理所有世界相关的初始化与状态切换; - 所有世界构造用的数据都被移动到了独立函数中,逻辑清晰分明;
- 后续模式管理可以统一使用类似结构,保持风格一致。
优势总结
这种结构化的处理带来多个好处:
- 职责分明:每种模式独立处理自己的数据和初始化逻辑;
- 切换清晰:通过
PlayXXX
函数快速切换,容易测试和维护; - 复用更高:以后如果有多个“世界”或“世界模板”,只需复用
PlayWorld
中的基础逻辑; - 灵活拓展:未来新增复杂界面(如多人房间、大地图等)也可以依赖此模式管理体系,无需杂糅。
目前我们已经构建好了“播放世界”的基础架构,为整个游戏模式系统的清晰划分与可维护性奠定了良好基础。接下来只需继续将其他模式依此标准逐步整理,即可形成高内聚、低耦合的游戏运行体系。
运行游戏以查看新代码的效果
我们目前进行的是运行游戏后的初步测试,主要目标是验证初始化时所调用的播放片头动画(cutscene)是否如预期工作,并进一步观察在未初始化世界(world)的情况下触发的行为。
当前状态与预期效果
- 游戏运行后,能够正常从初始界面淡出,随后淡入进入片头动画;
- 这是我们在初始化逻辑中通过调用
PlayCutscene
设置的,说明片头动画的相关初始化和模式切换逻辑是正确的; - 此部分流程与预期一致,属于已完成并运行良好的部分。
问题暴露:世界模式未初始化时的异常行为
- 在播放片头动画过程中,如果尝试按下某个控制按钮,程序会试图进入处理世界模式相关的逻辑;
- 但当前世界并未初始化,因此程序会进入错误的路径,尝试执行像“创建新英雄”这种依赖已初始化世界资源的操作;
- 由于
world
根本不存在,相关资源或结构体未分配,结果就导致出现了错误行为; - 此处并非真正切换到世界,而是切入了不该执行的分支。
分析与结论
- 出现错误的根本原因是:用户交互触发了某些依赖“世界”模式的代码路径,而当前并没有初始化 world;
- 游戏尚未处于 world 模式,也没有任何 world 数据可供操作,因此自然会导致逻辑错误甚至崩溃;
- 而反观 cutscene 部分的行为是正确的,说明我们为 cutscene 建立的初始化路径和状态切换逻辑是清晰且健壮的。
下一步计划
我们需要:
-
确保模式隔离完整
- 在当前模式(比如 cutscene)下,禁止响应属于其他模式(如 world)的操作;
- 增加逻辑判断,只有在 world 模式已激活并初始化后,才允许进入相关代码路径。
-
加入防御性检查
- 所有涉及 world 的访问逻辑前必须判断 world 是否已初始化;
- 若未初始化,应阻止进一步操作,并在调试模式下输出提示信息。
-
完善输入逻辑的模式依赖
- 把用户输入的处理逻辑根据当前的游戏模式进行分发;
- 例如,如果当前是 cutscene 模式,则输入事件只能触发 cutscene 中的推进逻辑,不能影响 world;
-
最终目标:统一的模式驱动结构
- 每个游戏模式拥有独立的输入、更新和渲染逻辑;
- 所有操作都通过当前
game_state.mode
的分支执行,不允许跨模式直接访问不属于当前状态的数据。
小结
当前 cutscene 流程运作良好,说明模式驱动机制的基本结构已建立;但未初始化世界的情况下仍能误入 world 模式逻辑,暴露了我们对模式隔离和输入处理的不足。下一阶段的工作重点就是增强对游戏模式的判断与切换机制,确保每种模式的行为严格受控,输入不越界,逻辑不混乱。
将game_state
中持久化的元素与特定于世界游戏模式的元素分开
我们目前正在对整个游戏架构进行整理与重构,主要目标是使“世界(world)”模块更清晰、更直观,同时将不同作用域的数据合理划分,以增强模块间的边界、降低耦合度。
明确状态划分:持久数据 vs 模式数据
我们对游戏状态中的数据进行分类:
-
应保留在全局游戏状态(game state)中的数据
- 如:当前参与游戏的玩家信息,即“谁加入了游戏”;
- 控制中的角色(
controlledHero
)可能是持久性的,意味着不管切换到哪个模式,都应保留该信息; - 因此这些信息会保留在主游戏状态结构中。
-
应迁移至特定游戏模式(如 world 模式)中的数据
- 其他与游戏世界具体运行相关的内容(物理对象、实体、粒子系统等)应属于“游戏模式 world”;
- 所有仅在某一模式下存在的数据都将转移到特定模式结构内,避免污染全局状态;
- 这有助于支持未来多模式架构,如 title screen、cutscene、gameplay 等互不干扰。
world 文件的重构策略
- 我们开始向
world
文件中迁移这些数据; - 初步会以一个统一结构
GameModeWorld
来承载所有和世界模式有关的状态; - 对于未来是否需要将
world
文件拆成多个部分,暂时不做决定,根据后续复杂度而定; - 目前策略是:先集中再细化。
初始化结构调整
- 游戏结构中现在有
metaArena
与modeArena
两个内存管理单元; - 但我们无法确认
metaArena
的具体用途,推测是历史遗留内容或用于 meta 层(例如选项、UI 状态等); - 由于当前并无调用
metaArena
的必要,我们将其移除,若后续有模块依赖此部分再进行补回; modeArena
将继续作为当前游戏模式内存资源的分配源。
数据结构移动及模块边界收敛
- 当前有很多与世界运行有关的数据结构,比如
lowEntity
,pairwiseCollisionRule
,particleCell
,particle
等等; - 它们原先可能被散落定义在多个模块中;
- 现在我们将它们集中移入到
GameModeWorld
所属的模块内; - 这样逻辑上它们的作用域更加清晰,未来也更容易维护;
- 不过这些结构体较多,未来可能继续细分成多个头文件或逻辑模块以优化组织结构。
后续方向
-
细化模块边界
- 将与物理、碰撞、粒子系统等相关的结构与逻辑分别独立拆出,形成粒度更细的模块;
- 目前先粗粒度合并,后续再逐步精炼。
-
统一初始化流程
- world 初始化将仅通过
InitializeGameModeWorld()
等统一接口完成; - 所需的内存、状态等都通过明确接口传入。
- world 初始化将仅通过
-
更强的状态隔离机制
- 游戏在运行中应仅操作当前模式下的数据;
- 避免任何模式间直接交叉访问状态,所有切换通过显式的状态迁移逻辑完成。
小结
我们正在对游戏的核心结构进行模块化重构,将状态根据用途与生命周期分为持久与模式特有两类。通过将与 world 模式相关的实体、碰撞、粒子等结构移入 GameModeWorld
,提高了逻辑清晰度,也为支持更多游戏模式、提升可维护性打下了基础。同时对内存管理进行了优化,移除未使用的 metaArena
,并重构初始化流程,使未来的拓展更加可控与明确。
改的有点多game_mode_world 还是先放game.h 下面
将一些内容移动到更合适的文件,并使编译器满意
我们目前正面临头文件之间相互依赖的问题,尤其是在定义 SimEntity
(模拟实体)与 World
(世界)相关结构之间,这些结构互相引用,导致编译顺序必须精确控制。这是 C 语言结构管理上的一大难点,也是我们这次重构中需要解决的核心问题之一。
问题概述
在处理 SimEntity
、WorldPosition
、LowEntity
等结构体时,我们发现它们在多个模块之间有相互引用的情况:
SimEntity
依赖WorldPosition
WorldChunk
等也依赖于LowEntity
- 由于头文件之间的引用关系,如果不按特定顺序包含,会出现前向声明不足或重复定义的问题
C 语言没有模块化支持,文件顺序决定了编译依赖关系,这使得多个结构的分离与组织变得格外麻烦。
初步解决方案
我们不打算大幅移动 SimEntity
或其他重要结构体的位置,因为这些结构通常非常核心、用途广泛。相反,我们采取了一个相对折中的策略:
-
临时移动
WorldPosition
的定义- 将
WorldPosition
移入sim_region
的头文件中,这虽然不是最合理的结构位置,但可以暂时解决依赖问题; - 通过这种方式,我们先解除循环引用问题,以便继续推进重构工作。
- 将
-
调整头文件包含顺序
- 明确
sim_region.h
应该先于world.h
包含; - 保证结构体定义的可见性符合依赖逻辑;
- 示例:
#include "sim_region.h"
放在#include "world.h"
之前。
- 明确
-
分离世界模式逻辑
- 创建新的文件对
world_mode
相关内容进行隔离,比如:game_world_mode.cpp/h
; - 把原本混杂在
game.cpp
中的世界模式内容独立出来,避免sim_region
和world
再次出现在同一个文件中,降低耦合度; - 虽然这种做法不是最优雅的,但在当前架构下已经是一个现实可行的折中方案。
- 创建新的文件对
其他模块清理工作
在整理头文件时,我们顺便进行了一些清理工作:
- 将与测试相关的代码(如
test_sand
,test_normal
)保留在当前文件中,准备后续彻底移除; - 将
random_series
移动到靠前位置(数学相关),放置逻辑更合理; - 把
audio_state
等尚未正式模块化的内容暂时保留在game.h
中,待后续设计更清晰的音频系统后再独立出去。
命名与结构统一化
- 对于访问游戏世界相关状态的接口,如
get_low_entity_state()
,我们统一通过game_state.world_mode
访问; - 暂时使用通用名如
world_mode
代表世界模式相关结构,虽然这些名字并不完美,但当前先以明确结构关系为主,后续再统一命名风格; - 重构命名是下一阶段的工作,我们优先保证结构可用、逻辑清晰。
总结
我们正在逐步解决由于 C 语言模块化缺失导致的头文件依赖困境,通过重新组织结构定义、拆分功能模块、合理安排包含顺序,来实现可维护性更强的代码架构。当前策略以解决循环引用问题、降低模块耦合、明确结构职责为目标,同时为后续更进一步的架构优化(如引入模块化工具或使用更现代语言特性)打下基础。虽然过程繁琐,但这是构建健壮系统不可或缺的一步。
新建
game_world_mode.h
game_world_mode.cpp
继续修复错误
调整初始化代码中的内存arena创建
我们在整理 GameUpdateAndRender
的初始化流程时,意识到目前的状态管理和内存分配方式存在一定的混乱,尤其是在音频系统和模式用内存(mode arena)方面。为了更清晰、系统化地管理游戏状态,我们开始对内存分配流程进行重构。
当前问题
- 音频状态的初始化和
mode arena
的初始化顺序不合理,音频初始化竟然在模式内存之后,逻辑上并不清晰; - 所有状态初始化都混杂在一块,缺乏结构性;
- 内存分配的粒度不明确,导致内存管理混乱;
- 很多模块只是简单地持有对 arena 的指针,但我们在初始化中并未显式地控制这些分配行为。
重构方向
我们决定引入一个更系统化的内存分配方式,统一使用一个 总内存区域(total arena),然后从中划分子区域(sub arenas),用于各个子系统:
1. 初始化总内存区域
我们首先初始化一个总的内存分配区:
MemoryArena totalArena;
InitializeArena(&totalArena, size, base);
这个区域将负责管理游戏中的所有临时与模块化内存分配需求。
2. 创建子区域用于音频
从 totalArena
中分出一个子区域专门用于音频系统:
MemoryArena audioArena = SubArena(&totalArena, Megabytes(1));
- 明确给音频系统分配固定的内存空间(例如 1MB);
- 保证音频系统的内存和其他系统分离,便于调试与控制;
- 初始化音频状态时,传入该子 arena 指针,使其使用这块专用内存。
3. 创建子区域用于游戏模式(mode)
继续从 totalArena
中划出另一块子区域,作为游戏模式运行的内存:
MemoryArena modeArena = SubArena(&totalArena, RemainingSize(&totalArena));
- 用于管理当前游戏模式中的各种状态,例如实体管理、世界状态等;
- 将
modeArena
传入游戏模式的初始化函数,替代之前不清晰的全局分配行为。
4. 音频状态结构的优化
音频状态结构本身应当管理其所需的内存,而不是依赖外部全局变量。因此在其初始化时,我们将其需要的内存从 audioArena
中分配,而不是事后硬塞进去。这种方式更符合封装原则,也减少外部干预。
后续调整计划
- 可能会将音频状态完全迁移到音频系统模块内,由系统本身管理其生命周期;
- 会逐步把其他系统(例如物理、UI)也切换为类似的内存管理模式,统一从
totalArena
中分配其专属子区域; - 所有模块之间不再共享 arena,而是明确各自的分配范围,减少耦合,提高内存可控性;
- 当前是整理的初步阶段,名字和结构仍可优化,目标是逐步建立起一个明确、整洁、系统化的内存布局结构。
总结
我们开始将初始化流程从混乱的结构中解耦,改为采用统一的总内存池,并在其上划分子区域,为各子系统独立分配资源。这不仅提升了系统的模块性,也为未来的扩展和维护打下了良好的基础。尽管目前还只是初步整理,但方向明确——通过精确的内存管理提升架构清晰度和运行时稳定性。
修复剩余的编译错误
我们已经完成了引导动画的优化,外观上看起来比之前更好,且比之前更加简洁。现在不再控制角色了,整个流程即将结束。音频环节也已经完成,并且现在处于游戏状态的“王者之国”中。对于指针的使用,发现已经不再需要它了,它的作用似乎已经消失。
接下来,控制部分被更新为删除实体、更新和渲染世界、瞬态状态连接等操作。这些操作有助于从游戏状态切换到游戏模式世界,所以目前的状态应该只剩下世界模式的部分。现在这部分工作已经完成,应该不再存在太多问题。
在这之前的所有操作都已经顺利完成,我们也修复了不少问题。现在,一切都看起来正常,并且控制部分已经不再引发任何异常,游戏的过渡已经变得更平滑。剩下的部分应该不会再有太多难题,整体上已经可以进行适当的停顿。
没时间了,运行游戏,世界模式暂时无法访问,直到后续完成清理工作
虽然现在已经稍微清理了一下,但还没有完全完成。由于时间有限,今天需要暂时停止,明天继续进行最后的整理。我们会确保一切运行正常,现在看起来一切都还可以,但目前游戏不会启动。明天的工作主要是完成剩余的清理,之后就基本可以完成任务。
问:内存区域是如何工作的?
目前,内存区域(memory arenas)只是一些内存块,这些内存块被划分为子块进行分配。具体来说,它们就像是从一个巨大的内存块开始,每次有人需要分配内存时,就从当前空闲的位置开始分配,将需要的内存放置在下一个可用的位置。这就是内存区域目前的工作方式。
问:既然没人提问,那你听说过AMD开源了很多他们的GPU相关内容吗?
我没有听说过AMD将其大量GPU相关的内容开源的消息,不过如果真的是这样的话,对他们来说可能是个很好的主意。因为传统上,AMD面临的最大问题之一就是驱动程序,特别是无法解决驱动相关的问题。所以让更多的人关注这个问题,可能对他们来说是一个好事。但由于我没有看到你提到的具体内容,所以不确定是否是指这个问题。
问:世界模式的切换应该像过场动画的切换一样进行预加载吗?
关于预加载和过场动画的变化,确实可能是这样的。这也是我之前提到的,在设置模式时清除当前模式可能并不是最终的做法。可能会改成在切换模式之前先创建一个新模式,并让它们在后台挂着更长时间。这样做还有其他一些原因,比如如果想要在模式之间进行交叉渐变或者类似的操作,这样的方式会更好。不过,在做这些之前,至少要先完成对模式的分区处理。
问:那么“元游戏”是指像游戏前的准备工作之类的东西吗?
“元游戏”是指所有不直接涉及实际游戏玩法的部分,比如游戏开始前的内容,或者游戏中非核心玩法的部分。总之,任何不属于游戏本身直接进行的操作都可以算作是元游戏。
问:嗨!我刚完成了计算机科学的第一学期!刚学了一些基础的Java,但我想知道你有没有什么前进的建议。另外,你知道哪些大学的计算机科学部门很优秀吗?
刚刚完成了计算机科学的第一个学期,学习了一些基础的Java语言。对于接下来的学习,建议尽量多花时间进行自己的编程实践。如果你专注于游戏编程,那么可能不需要继续用Java,因为Java并不是用来开发游戏的主流语言。你应该学习C和C++,因为这两种语言更常用于游戏编程和其他系统级开发。
关于大学方面,抱歉我不太了解学校内的计算机科学课程。我的经验主要集中在编程实践上,而不是学校课程。如果你想进一步发展,可以考虑自己进行更多编程练习,尤其是在游戏开发方面。
说:抱歉,今晚慢了点,聊天突然爆发成了关于游戏中排除选项菜单的争论 😛
提高了游戏中的选项菜单执行方式,涉及到一些优化和改进。虽然这些更改可能会引发讨论或争议,但这是正常的,特别是在游戏开发中。关于是否能采取其他方式来改进这个部分,仍然值得进一步思考和探索。
问:我想知道为什么把游戏模式的结构体放入一个联合体?
将三个游戏模式放入一个联合体(union)是因为每次只会使用其中的一个模式。这样做的目的是为了节省内存和提高效率,因为不需要同时存储所有游戏模式,只需要存储当前正在使用的那一个模式。此外,使用联合体可以让程序更加简洁和高效。
问:我有一个通用的USB控制器,它没有被XInput检测到,有什么方法可以检测它吗?使用DirectInput?还是尝试使用Xbox控制器模拟器之类的?
如果控制器没有被XInput检测到,可以尝试使用DirectInput来检测控制器。如果DirectInput无法使用,也可以尝试使用Raw Input并将其作为HID设备来访问,不过这种方法需要进行更多的解析工作,因此会相对复杂一些。如果想要简单一些,可能需要考虑使用Xbox控制器模拟器来帮助检测控制器。