Blackboard:动态控制类似蛇的多节实体
我们目前正在处理一个关于实体系统如何以组合方式进行管理的问题。具体来说,是在游戏中实现多个实体可以共同或独立行动的机制。例如,我们的主角拥有两个实体组成部分,一个是身体(或称躯干),另一个是头部。这两个部分在某些情况下需要一起行动,而在其他情况下(比如受到某种魔法攻击)也可能会分离,各自独立运作。
我们意识到,这种“组合实体”的需求不仅仅存在于主角身上,也可能出现在敌人或其他复杂生物中。比如:
- 一条蛇形怪物,拥有一个头部和多个身体段,每个部分可能占据不同的格子,头部移动时,身体段需要跟随移动。如果某个部分被攻击切断,那就会断成两截,各自继续移动。
- 一个大型敌人可能有主体和多个附属的“手臂”或其他攻击部件,甚至可能有围绕它旋转的小型子实体。这些部分有时需要统一控制,有时也需要单独处理(比如被破坏后分离)。
我们的目标是设计一种系统,能灵活地支持这种实体的组合与解耦,不需要提前固定它们的结构,而是允许在运行时动态决定它们的行为关系。我们希望避免将控制逻辑分散到多个地方。过去尝试的方法是让实体之间相互了解彼此的存在(例如,身体知道头部是谁,头部也知道身体是谁),但这种双向耦合让代码变得不自然、不好维护。
我们反思后发现,这其实是一种典型的“面向对象陷阱”:人为地将逻辑拆分到对象内部,强行以“每个实体自己更新自己”的方式编写代码。这样做的问题在于,很多行为其实跨实体才有意义,比如头和身体协调移动,如果逻辑分散,就必须在多个地方处理相互之间的通信,显得既不直观也不高效。
更合理的方式是:直接以“组合控制器”的思维来处理,比如写一个“头-身体控制器”,专门处理这种组合。它内部检查哪些部分存在,然后统一更新。这样既避免了分散的逻辑,又提高了灵活性。即使没有头部,那控制器也只是跳过头部逻辑即可,不会导致错误。
我们希望把重点从“每个实体控制自己”转向“控制器跨实体统一处理”。这个方式更加贴近我们在实际程序开发中常用的思维方式:关注算法本身,而不是被人为拆解的对象边界。
目前我们正在尝试实现这一新想法,先从一个具体例子出发,比如重新实现主角的头和身体控制逻辑,看看是否能更清晰简洁地表达意图。之后,我们计划实现另外一个案例,比如蛇形怪物,用于验证这个架构是否具有普适性。如果能支持多个不同场景,我们就可以确认这种架构是更为合理和健壮的。我们的目标是建立一个可扩展、灵活、代码结构自然的实体控制系统。
Blackboard:控制器模板
我们目前的实体系统采用了“模拟区域(sim region)”的机制。在这个机制中,世界被划分为多个区块(chunks),当需要模拟某一区域时,就会将该区域的所有区块中的实体加载并“解包”到当前模拟区域中。在这个过程中,我们会将所有实体插入一个哈希表中,以记录它们的 ID,以便其他实体可以通过 ID 来引用它们,比如实现“追踪某个目标”的行为。
这种“实体引用”的方式在某些情况下依然是合理的,尤其是当我们只需要表达一个实体观察或攻击另一个实体时。但对于多个实体需要作为一个整体来协同工作时,这种机制就不够用了。我们的问题主要出在这部分——多个实体组成一个统一的操作单元,比如“一个身体加一个头”这种组合,我们需要找到更自然的解决方法。
于是我们提出了一种新的设计思路:引入“控制器模板(controller templates)”。这里的模板并不是 C++ 模板,而是更通俗意义上的“模式”或“结构蓝图”。每个控制器模板代表一个可供实体组合的逻辑结构,例如:
- 英雄控制器:包括一个头部槽位、一个身体槽位;
- 蛇型怪物控制器:可能包含多个身体段槽位、一个头部槽位;
- 其他复杂生物控制器等。
在这种新系统中,只有那些可以被控制器管理的实体,才会拥有一个额外的数据结构,我们可以称之为“控制器标签(controller tag)”。它包含以下几个关键字段:
- 模板类型(Template Type):表示该实体应该归属于哪个控制器模板,比如英雄控制器、蛇控制器等;
- 实例 ID(Instance ID):表示当前这个控制器实例的唯一标识,多个实体拥有相同的实例 ID 意味着它们属于同一个控制组;
- 槽位(Slot):表示该实体在模板中的具体位置,比如是头、身体、手臂,或者是某个数组槽位(支持多个实体插入)。
这些信息是每个实体自身带有的,而不是额外维护的集中数据结构。我们再维护一个以实例 ID 为键的控制器实例哈希表,用于在模拟区域解包实体时动态构建控制器。当一个实体被加载进模拟区域时:
- 我们检查其控制器标签;
- 然后根据标签中的实例 ID,在控制器哈希表中查找是否已有对应的控制器实例;
- 如果有,就将该实体放入指定的槽位中;
- 如果没有,就创建一个新的控制器实例并加入该实体。
这套机制具有高度的灵活性。例如:
- 如果我们想让某个实体“拾取”另一个实体并将其作为自己的手臂,只需要修改该实体的控制器标签中的模板类型、实例 ID、槽位即可,无需做额外的绑定逻辑;
- 如果某个组合体中的某个部位(比如手臂)被打掉,那么该实体依然存在,只是脱离了原控制器组;
- 如果之后原主体重新靠近这个“断肢”,它的控制器仍能识别该实体是自己的一部分,因为它的标签信息仍然存在。
这个系统没有任何额外的引用维护成本,也没有清理或生命周期管理的负担。实体的控制组是每次模拟时根据其标签信息自动组合的,不依赖于运行时的持续状态,因此也不会出现失效引用或资源泄漏的问题。
这种机制从根本上避免了面向对象带来的结构僵化问题。我们不再为每个实体都构造一个特定的控制逻辑,也不需要维护复杂的双向引用关系。控制器的逻辑是集中统一的,按模板组织而不是散布在实体之间,从而保持了代码的简洁性和自然表达力。
因此,我们决定采用这个设计方案,重构之前关于组合实体的实现,并放弃昨天那套基于互相引用的逻辑方式。下一步我们将清理掉之前的相关改动,并开始搭建这个新的控制器模板系统,以验证它的有效性与可扩展性。
game_entity.h:移除 entity_relationship
和 ReferenceIsValid
,以及实体结构体中的 PairedEntityCount
和 PairedEntities
我们在处理 sim region(模拟区域)与 entity(实体)结构时,决定对之前的设计进行调整和修复。
我们原先引入的一套关系系统(relationship system),现在决定彻底移除,不再在实体之间显式存储这些关系。取而代之的是,回归之前的做法,重新采用实体引用(entity references)的方式来表达实体之间的联系。
具体修改如下:
- 原有的实体关系系统将完全移除;
- 不再在 entity 内部保留任何用于描述关系的字段或结构;
- entity references(实体引用)机制将作为保留功能继续使用,即实体之间通过引用其他实体的 ID 来实现追踪、攻击、观察等交互逻辑;
- 验证引用是否有效的逻辑,也无需再考虑是否存在“关系结构”,这一部分相关的判断逻辑也将一并清理;
- 所有与关系相关的字段、检查函数、结构将不再需要,被清除出代码;
- 最终的替代方案,是以“模板控制器”(template controller)的方式组织逻辑结构,而不是用显式的实体关系来维持结构化的整体。
此外,正在构思的模板系统将被逐步引入,作为原本关系系统的逻辑替代核心。具体细节还在调整,但可以明确的是,实体之间将以更灵活、更轻量的方式进行组合和控制。我们将在接下来的流程中继续推进这一系统的实现与集成。
game_entity.h:引入 entity_controller_type
、entity_controller_slot
和 entity_controller_id
,并在实体中添加这三项
我们现在在 entity
中引入了一个全新的概念——“控制器类型”(entity_controller_type
),目的是为了在逻辑上组装出更复杂的“生物体”或“组合体”。这是对传统实体关系模型的进一步简化与抽象。
具体设计如下:
控制器类型
我们定义了一个枚举类型的控制器类型字段(entity_controller_type
),用来表示当前实体是否参与某种控制器结构,示例如下:
None
:表示该实体不参与任何控制器结构;Hero
:代表它是一个“英雄”控制器的组成部分;Snake
:为将来可能的“蛇形”控制器预留。
控制器的作用是将多个实体在模拟区域中逻辑地组合成一个整体结构,用以统一行为控制、物理模拟或逻辑运算。
控制器插槽(Slot)
我们为实体加入一个 controller_slot
字段。这个字段用于指明该实体在其所属控制器中的位置或角色。当前将其设计为一个简单的整型索引,具体用途将在后续通过控制器代码定义。
例如:
- 一个英雄控制器可能包含多个插槽:头部、身体、左臂、右臂等;
- 实体通过指定插槽编号来表明它在结构中的位置;
- 插槽机制未来可以扩展为支持数组插槽(比如一个多头怪有多个头)。
控制器实例 ID(Instance ID)
此外,我们引入了 controller_instance_id
字段,该字段用于将多个实体归属于同一个控制器实例。也就是说,只要多个实体拥有相同的 controller_instance_id
,它们就会在模拟时被组装成一个逻辑整体。
当前,我们暂时将 controller_instance_id
和 entity_id
分开处理,避免逻辑混淆。它们来源不同、使用范围不同,不应该互换使用。控制器实例 ID 仅用于组装逻辑结构,不涉及实体唯一标识系统。
元实体(Meta-Entity)的可选性与数据结构
我们还未完全决定“控制器本身”是否需要拥有独立的数据结构或存储空间。
当前设想是尽量避免为控制器本身保留冗余数据。我们更倾向于控制器的整体行为由其组成的子实体决定,例如:
- 如果一个“头部”实体拥有智能相关的属性,那么整个组合体就表现出智能;
- 一旦头部被移除或破坏,整体智能能力就丧失;
- 这使得整体行为更自然、动态和模块化,不需要在中心结构中强行保留某些全局设置。
如果未来发现控制器本身确实需要存储特定的状态或数据结构,我们可以通过设置独立的“控制器实体”并将其赋予一个特定 ID 来实现。但目前保留这一点为后备方案。
总结
我们正在以更灵活、松散耦合的方式替代传统实体继承关系或显式组合结构:
- 每个实体通过控制器类型、插槽编号和控制器实例 ID 进行动态组合;
- 所有结构在模拟区域加载时按需拼装,无需静态绑定或生命周期管理;
- 系统更加有机、模块化、无状态,允许实体被自由组合、断裂和重组。
接下来将继续推进代码实现,通过先行编写具体控制器逻辑(例如 Hero Controller)进一步验证这一机制的实际效果。
game_entity.h:思考这个系统将如何替代 entity_type
我们现在正在对整个实体系统进行结构性的重构,目的是彻底摆脱“实体类型”的概念,用一种更灵活、更模块化的控制器机制来代替。这种机制不仅简化了架构设计,也增强了系统的可扩展性和动态行为表现。
以下是我们当前的核心设计理念和具体变动:
1. 实体不再有“类型”
我们不再为实体分配固定的“类型”(如 Hero、Enemy、Glove 等)。实体本身不携带类型标识。取而代之:
- 实体的“类型”由当前附加在它身上的控制器决定;
- 控制器才是唯一控制实体行为的载体,一个实体只能同时被一个控制器操控;
- 实体的属性和行为不是由“我是什么”决定,而是由“我正被谁控制、我正处于何种结构中”所决定。
这种设计思路强调了行为的外部控制性而非内部本质性。
2. 控制器即行为装配器
- 控制器是一种组合逻辑:多个实体被聚合到某个控制器中,形成一个统一体;
- 每个控制器有自己的插槽系统,决定每个部位对应的实体;
- 控制器可以是玩家控制器(hero controller)、AI 控制器(如 snake controller)等;
- 控制器的设计允许动态更换实体,例如一只手断掉之后,可以被其他控制器重新拾取和使用;
- 控制器是当前行为的“主脑”,实体只是组成部件。
3. 结构灵活、可重组
- 我们将结构设计得更像“拼装生物”;
- 比如,一个手套(glove)不再是“手套类型的实体”,它只是正好被某个控制器使用在 glove 插槽中;
- 一旦该实体脱离控制器,它不再具有任何“类型”意义,只是一个普通组件;
- 其他控制器可以接管它,并在不同上下文中使用它,体现出极高的行为灵活性和重构能力;
- 系统支持组合体断裂、合并、替换等动态操作。
4. 类型信息被拆散为多种属性
由于取消了统一的“类型”字段,我们需要将实体的原始“类型含义”拆解为多个独立系统进行控制:
- 绘制逻辑(rendering)不再由类型决定,而由当前控制器+插槽上下文决定;
- 行为逻辑(AI、输入响应)由控制器脚本决定;
- 模拟逻辑(碰撞、物理等)由控制器和其聚合结构决定;
- 控制权归属:通过控制器哈希(controller hash)字段标识每个实体当前隶属于哪个控制器。
5. 引用系统调整
我们仍然保留实体引用(entity reference)的系统,但用途被局部化和场景化:
- 不再用于表达控制器—实体的配对;
- 而是用于表达如“目标引用”、“攻击目标”、“当前追踪对象”等临时逻辑行为;
- 实体之间不再需要双向绑定引用,也不再有长期耦合;
- 控制器取代了原有的结构性连接,成为唯一的组合机制。
6. 哈希系统更新
- 原有的
entity_hash
用于标识单个实体; - 新增了
controller_hash
字段,用于标识控制器实例; - 每个控制器通过哈希值管理其结构内的所有实体,支持快速查找与更新;
- 虽然叫“entity_controller”,但我们目前只定义这一类控制器,不涉及其他控制器系统,因此命名暂不修改。
总结
我们正在构建的是一个完全解耦、模块化、可重组的实体-控制器系统:
- 实体仅为物理/逻辑组件,没有固有类型;
- 控制器承担逻辑聚合、行为协调和状态分发;
- 结构变化实时同步、自动响应;
- 彻底摆脱传统面向对象实体建模中“类型中心论”的束缚;
- 架构更自由、抽象层更清晰,具备极高的灵活性和未来扩展潜力。
接下来的工作将集中于实现第一个控制器样例(如 Hero Controller)以验证整体系统设计的合理性。
game_entity.h:将 entity_controller_*
重命名为 brain_*
我们觉得用“controller”这个词不太合适,因为它容易让人联想到游戏手柄控制器,所以想换一个更贴切且独特的名字。考虑用“brain”(大脑)来替代,这样能更好地表达它作为实体行为和决策核心的角色。虽然“brain”也不是最完美的描述,但至少能够唯一且明确地区分开。
在设计上,我们定义了一些“槽位”(slots),每个槽位就是一个“brain”的位置,用来组织和管理不同部分的行为逻辑。接下来会把每个槽位进行具体实现,比如先把它们设为二分之一(50/50)这样的结构,方便调试和测试。接下来会写代码来实现这些想法。
总体来说,就是从“控制器”概念转向“脑”这个概念,强调这是实体的思考和控制中枢,提升语义的准确性和系统的直观性。
game_sim_region.h:引入 brain_hash
并在 sim_region
中添加一个 brain_hash
数组
我们讨论了“brain hash”的概念,这个名字听起来有点像某种不太好的东西,但它实际上是指向我们定义的某个“brain”(脑)实体的指针。这里会有一个“brain ID”,用来标识不同的脑。这个ID其实就是一个索引,表示不同的脑实例。
考虑到这些脑实例的数量不会很多,所以不太适合用过大的哈希表,比如2的幂次方大小的哈希表。未来可能会考虑用质数大小的哈希表,并用取模操作来处理,但这只是细节问题,随时可以调整。
关于哈希的实现,这里使用的是内部链式哈希结构,可以处理哈希冲突和重复项,逻辑上是合理的。实体哈希里也可以直接存储实体本身,但这可能会影响缓存性能,需要权衡。
脑的集合会随着实体加入和分配控制器ID逐步组装完成。考虑到维护这些哈希结构的开销,尤其是清理过程的成本,哈希表的大小和清理策略是性能的关键点。清理可以采用增量式的方式减轻每帧的开销。
控制器ID中预留了0作为“无控制器”的特殊值,这样可以简化判定。因为有了这个0值,脑类型(brain type)本身可能不需要专门的空值标记。如果控制器ID是0,脑类型可以不设置,简化了逻辑。
在模拟的执行步骤中,涉及了身份识别(identity)相关的调用,但现在看来这部分代码可能已经很少被用到,有可能会被简化或移除。
总结来说,整个设计的核心是通过脑哈希来管理实体的控制器状态,用哈希结构高效地查找和管理这些控制器,同时预留灵活的扩展和优化空间。
game_world.cpp:将 PackEntityReferenceArray
改名为 PackEntityReference
,并只处理一个实体
我们在清理之前的代码损坏,特别是处理实体引用相关的部分。现在实体引用的数组结构基本不再使用了,改为更简单的形式。核心逻辑是判断实体引用是否有有效的指针,如果有指针,就把引用的索引设为指针所指实体的ID;如果没有指针,则根据具体情况清除或者保留索引。
具体来说,如果当前模拟区域(sim region)中没有包含被引用的实体,则认为这个引用应该被移除。还需要判断该实体是否被删除,删除的实体引用也要清除掉,以避免引用已经不存在的实体。
在“打包”(packing)实体引用数据时,实际上并没有真正压缩数据,而是为了保持引用的指针和索引同步做的操作。这个过程可能比较临时,但目的是确保引用状态的正确性。
还有一个复杂情况是:如果引用的实体从模拟区域移出,即不再可见,指针会变成空,但索引可能仍然存在,这时需要区分索引是因为实体出区域而暂时不可见,还是因为引用已经被显式清除。如果引用被清除了就要删除索引,否则保留索引。
但打包时并不总有模拟区域的信息,比如世界创建阶段就没有模拟区域,所以需要有机制允许传入模拟区域参数,如果没有传入,则跳过相应检查。
整体来说,这套机制的目标是保证实体引用的有效性和一致性,能够正确处理实体被删除或移出模拟区域的状态,同时保证引用数据在不同情况下合理更新和清理。当前逻辑还有一些不确定性,后续需要继续调整和完善。
game_world.cpp:让 PackTraversableReference
和 PackEntityIntoChunk
接收 sim_region
作为参数
我们目前正在处理模拟区域(sim region)相关的数据传递问题,尤其是实体引用在打包(pack)操作中如何正确保留或清除的问题。当前的设计是把模拟区域参数传递到引用处理的相关函数中,目的是为了判断引用的目标实体是否还存在于当前模拟区域中。
这种传递方式已经在多个地方被使用,我们在多个函数中将模拟区域作为参数传入,以便可以对照区域内的实体状态进行逻辑处理。但整体实现方式仍有些不够优雅,有一些结构显得“脏”(不干净、不规范),存在结构臃肿、逻辑重复或者耦合度过高的现象。
我们目前还没有一个特别聪明的替代方案来简化这个流程,也不确定是否有更优雅的方式可以替代模拟区域参数的传递。所以只能在现有结构下继续推进,边做边调整。
接下来继续处理“是否已删除”的判断逻辑,也就是is_deleted
相关的检查机制。这个机制的作用是在引用打包过程中识别已被删除的实体,并将其引用清除,以防止后续逻辑中访问已无效的实体。
我们正在逐步完善整套实体引用系统,目标是确保在动态模拟过程中,实体引用始终保持有效且同步,尤其要正确处理实体被删除、退出模拟区域或被更换控制结构等各种边界情况。当前的实现虽然还有不足,但整体结构已在逐渐成型,并为后续更复杂的系统交互奠定基础。
game_entity.h:引入 IsDeleted
成员
我们现在正在处理实体删除状态的判断逻辑。当前,我们使用一个实体标志位(entity flag)来表示实体是否被标记为已删除,即 EntityFlag_Deleted
。因此,我们的删除检查函数 is_deleted
实际上只是对这个标志位的封装。
这个函数的实现非常简单:它接收一个实体作为参数,然后检查该实体的 flags
字段中是否包含 EntityFlag_Deleted
。也就是说,函数本质上等价于调用 is_set(entity, EntityFlag_Deleted)
。
由于系统中已有一个通用的 is_set
函数用于检查实体是否具备某个标志位,因此我们其实只是借助这个现成函数来实现对“已删除”状态的快速查询。
这种方式保证了判断逻辑的统一性与可维护性,也简化了上层逻辑在处理实体删除情况时的判断流程。只要一个实体被标记为 Deleted
,相关逻辑就能自动识别并作出对应处理,比如在引用打包时剔除对已删除实体的引用。
接下来的操作将继续围绕引用系统与模拟区域的整合进行,使系统能在不同运行阶段正确识别哪些引用仍然有效、哪些应当被清除。这个过程虽然技术上不复杂,但需要保持逻辑严谨性,确保实体生命周期与引用系统之间的一致性。
game_sim_region.cpp:将部分逻辑从 LoadEntityReference
移至 GetEntityByID
并由其调用
我们目前在处理通过 ID 获取实体哈希(get hash from ID
)的逻辑时发现了一个不一致的问题。get entity by ID
和 get hash from ID
这两个函数看起来本质上应该是做同样的事情——根据一个 ID 找到对应的实体。但是它们内部的处理方式却不一致。
我们意识到,这可能是架构变动过程中遗留下来的冗余实现。随着系统不断重构,有些函数其实已经可以统一起来使用同样的底层逻辑,而不是各自实现自己的方法。
我们倾向于将 get hash from ID
简化为直接调用 get entity by ID
,这样不仅可以减少重复代码,还能使行为更一致。get hash from ID
完全可以作为一个更轻量、便于复用的接口存在,它最终只是对实体引用的快速索引。
虽然我们也考虑了是否把它实现为一个“free函数”(free function),用来更灵活地封装这类功能,但目前我们决定先以直接方式来处理它,也就是简单封装调用 get entity by ID
,保持实现清晰和简单。
这个阶段主要在清理、统一逻辑接口,消除过时的代码和结构分裂。接下来我们还将对整套实体引用、哈希和模拟区域之间的整合做进一步的理顺,以确保每个功能模块的职责清晰,行为可靠。
game_sim_region.cpp:让 PackEntityIntoWorld
接收 sim_region
我们目前正在处理 pack_entity_into_chunk
函数的参数问题。我们注意到它现在只接受一个 sim_region
参数,而不是三个参数。这是正确的行为。
在当前设计中,我们引入了 sim_region
(模拟区域)作为集中数据结构,用于包含与该区域相关的所有实体哈希(entity_hash
)和其他上下文信息。因此我们不再需要将多个哈希表单独作为参数传入,比如 entity_hash
、controller_hash
等,因为它们已经被统一包含在 sim_region
之中。
我们还检查了多个调用 pack_entity_into_chunk
的位置,发现有些地方传入的是 0
(即空值或无模拟区域的情况),这些地方通常是不会存在 sim_region
的上下文,比如在“将实体插入到世界中”这一类初始化或预处理操作中。在这些场景中,模拟区域尚未建立或不适用,因此函数不会传入 sim_region
。
而另一些调用点,比如处理实体更新的部分,则明确地传入了有效的 sim_region
。我们通过对比可以确认,现在的接口设计是统一且合理的。没有传入 sim_region
的调用是因为其运行环境中确实不存在模拟区域;有传入的场景也对应的是需要查找或写入区域数据的上下文。
这个变更是我们结构重构的一部分,目的是减少函数参数数量、简化接口设计,并通过 sim_region
这种集中式结构将相关依赖统一管理,提高可读性和维护性。接下来我们会继续清理旧代码,确保所有地方都遵循这套新的参数传递方式。
game_world_mode.cpp:从 AddPlayer
中移除 BodyRefs
和 HeadRefs
,为 Body 和 Head 初始化 brain_id
和一些 brain 数据
我们当前正在进行结构重构,目的是进一步优化实体系统的初始化与关联逻辑,特别是与“brain ID”(大脑标识符)相关的部分。
首先,我们已经废弃了一些旧的机制,例如旧的哈希表相关字段不再使用了。这些字段已经被统一纳入更现代化的 sim_region
(模拟区域)结构中管理,因此旧机制已经完全不再必要。
现在,我们的目标是在新建实体时,为其分配一个 brain ID
。我们采用的是一个简单的分配策略:调用一个类似于“添加 brain”操作的函数,该函数不需要立即创建一个完整的 brain 实例,它只是返回一个可用的 ID 令牌,供后续逻辑引用。
接着我们回到实体(entity)本身。我们会在实体结构中设置几个关键字段:
controller_type
controller_slot
- 以及
brain_id
这些字段都在这个初始化阶段被赋值。特别地,我们还引入了一个概念叫 brain_slot
,但目前我们对它的具体用途和设计方式还在探索中。暂时先按照简单思路来处理,即将不同用途的实体(例如:人物控制器与 AI 控制器)分别放入 slot 0 和 slot 1,虽然这不是最终实现方式。
我们目前不打算通过手动记住具体 slot 索引值来管理这种绑定关系,因为这样不但容易出错,还会导致系统难以维护。我们希望未来可以设计一个更干净、自动化的机制,避免硬编码 slot 编号或依赖大量的枚举定义,从而提升结构的可读性和可扩展性。
综上所述,这部分重构的重点在于:
- 废除旧字段,转向统一结构管理;
- 实体创建时分配独立
brain_id
; - 建立 controller 与 brain 的绑定关系;
- 避免 slot 硬编码,通过更直观的设计提升系统健壮性。
未来我们会继续调整 brain_slot
的处理逻辑,使其更简洁高效。
game_world_mode.cpp:引入 AddBrain
函数
我们正在实现一个“添加 brain”的方法,其核心功能非常简单:只是生成一个新的 brain ID
,并不会立即创建完整的 brain 对象。具体做法是直接使用当前存储结构中的计数器(类似于 last used storage index
)来生成一个新的 brain ID
。随后,这个 ID 就可以作为引用,和其它需要控制逻辑的实体进行关联。
接下来,我们处理 index_map
和 identity
相关的逻辑。由于架构发生了变化,这部分代码也需要做出调整。其中,index_map
是用来将某个标识符映射到相应实体索引的结构。我们更新了它的使用方式,确保和新的 brain 管理逻辑兼容。
当前令人振奋的地方在于,我们开始摆脱了之前繁琐的控制器数据流传递方式。之前的设计里,为了支持多种控制器和实体组合,我们不得不通过各种中转结构来“传递”和“找回”控制器的数据,例如把控制器绑定到实体,再在后面找回并使用。这种设计很容易导致代码混乱,也不够灵活。
现在我们构建的新机制,将支持一种更自然、统一的控制流程。设想我们在编写模拟逻辑时,只需要像编写正常逻辑那样,直接遍历所有的 brain,然后让它们控制各自的实体。例如,对于玩家实体,只需“对每个 brain 执行一次决策逻辑”,然后继续模拟世界,不需要显式从结构中“取回”控制器,再绑定回实体,这样的中转流程将完全不需要。
总之,新的系统设计具备以下优势:
- 统一逻辑接口:不管是玩家控制、AI 控制,或是其他行为逻辑,所有实体都通过统一的 brain 接口处理;
- 更自然的流程控制:控制逻辑更像传统的思维模型,比如“让每个 brain 做出决策”,而不是绕弯子去绑定/解绑/传递控制器;
- 减少中转结构依赖:消除了大量中间数据结构(如 controller mapping)的负担,提升整体系统的清晰度;
- 代码更简洁:便于未来扩展行为种类(如 AI、物理驱动等),无需针对每种控制方式写特殊分支。
接下来我们还需要进一步回顾 sim_region
的实现细节,特别是它是如何组织 brain 实体的,以便高效地完成遍历和处理。这个机制是整个控制系统流畅运行的关键。
game_sim_region.h:在 sim_region
中添加 MaxBrainCount
、BrainCount
和 *Brains
我们目前需要为系统中所有的 brain 实体建立一个统一的数据结构管理方式,其思路与管理实体(entity)的方式类似。也就是说,我们将设定一个最大 brain 数量(max_brain_count
),再配合一个实际计数器(例如 brain_count
或 used_brain_count
)和一个 brain 数组(brains[]
)来进行管理。
这套结构会在模拟区域(sim region)初始化时被创建或清空,在任何实际处理逻辑执行之前就准备好。这样我们就能确保在进入更新流程前,所有 brain 的存储空间已预先分配好,访问时具有良好的内存局部性,效率更高。
此外,brain 也将拥有自己的哈希表结构,这个哈希表会作为辅助索引机制,帮助我们快速通过 ID 查找具体的 brain 实例。其内部实现可能和实体使用的哈希表一样,采用链式冲突解决方式。
因此,我们可以总结为如下结构:
-
静态预分配
在内存中预分配一个固定长度的 brain 数组,长度为max_brain_count
。 -
计数器跟踪
使用brain_count
来追踪当前实际已用的 brain 数量,用于分配新的 ID。 -
哈希表索引
提供brain_hash_table
来支持通过 ID 快速查找 brain,哈希冲突时采用链式结构解决。 -
初始化顺序
在 simulation region 开始处理前,优先初始化 brain 数据结构,以确保后续操作能直接访问。
这套机制的好处是与实体管理逻辑保持一致性,使得 brain 可以像实体一样参与系统的通用逻辑流程,同时也便于维护和调试。后续处理过程中,任何需要访问 brain 的地方都可以通过该系统快速获取所需数据,无需特殊处理逻辑,从而提升系统的可扩展性和一致性。
game_world_mode.cpp:在 UpdateAndRenderWorld
中循环处理所有 brains,执行其逻辑
我们在模拟区域(sim region)中管理 brain 实体的方式基本成型,接下来我们会对每一个 brain 实体进行迭代处理。具体实现上,我们会遍历 brain 索引列表,每一个索引对应一个 brain 实体:
-
遍历 brain 索引
通过sim_region.brain_cap
得到总容量或计数,使用一个循环遍历这些索引。 -
获取 brain 实体
在每次迭代中,我们通过sim_region.brains[brain_index]
取得当前的 brain 实体指针,准备对其执行逻辑操作。 -
根据 brain 类型执行逻辑
每个 brain 都有一个类型标识字段,例如Brain_Hero
,Brain_Snake
等。我们根据其类型选择对应的逻辑执行路径:Brain_Hero
:玩家角色的行为控制逻辑;Brain_Snake
:可能是某种 AI 或其他单位的行为;Brain_Familiar
:一种附属单位,是否是单独的 brain 仍待确定;- 其他未知或未定义类型则落入默认处理路径(可能为空或报错处理)。
-
行为代码的位置
各类 brain 的逻辑会集中在对应的分支中处理,当前Brain_Snake
的代码尚未编写,Brain_Familiar
也未完全决定其行为模型是否独立。未来可能还会加入新的行为字段,用于指示当前行为状态(如攻击、巡逻、待机等),这些将作为单独结构维护。 -
控制器输入采样
对于需要用户控制的 brain(例如 hero),我们会采样控制器输入,用于决定行动。这部分逻辑目前已存在于某段代码中,我们考虑将这段逻辑提取为独立模块,以便统一调用,提高整洁性。 -
Hero 控制结构的调整
原来 Hero 有 head 和 body 两个实体,分别处理,现在我们可以将两者合并为一个整体处理,避免重复冗余逻辑,使得控制流更清晰。具体整合将在编译错误修复后再进一步推进。
整体思路是构建一个统一的 brain 执行框架,按类型分派对应行为逻辑,使整个模拟过程更清晰、更灵活,并预留扩展接口以便后续添加更多行为状态与处理模式。这个结构也有利于以后引入调度系统或多线程处理。
game_sim_region.h:在 brain
结构中添加 brain_type
我们当前在模拟区域(sim region)中加入了用于管理 brain 实体的数据结构。虽然目前这些 brain 相关的结构和逻辑仍然临时放置在模拟区域的实现文件中,但接下来我们打算将其独立出来,放入单独的文件以提高模块化和可维护性。现在暂时不动,但未来一定会进行这一步。
接着我们需要处理其他相关结构中的一些“parity count”之类的字段或机制。这类字段通常用于跟踪对象的版本、同步状态或变化次数等,是数据一致性检查或更新流程中关键的一环。
总的来说,当前阶段的主要任务包括:
-
brain 结构整合在 sim region 中
当前所有与 brain 有关的结构和逻辑都临时写在 sim region 内部。这虽然便于开发调试,但长期看不利于代码清晰度和逻辑分离,因此后续我们会将这部分抽出,形成独立模块。 -
准备迁移但尚未执行
我们明确知道这部分内容应该转移出去,但在进行结构稳定和基本功能完善之前暂时不会移动。当前重点是先保证逻辑正确和流程跑通。 -
parity count 的存在表明有版本控制
我们还需要处理或重构一些现有的 parity count 相关逻辑,这可能用于判断某些数据是否需要更新,或是否处于一致状态。这种机制将在整合 brain 管理与实体同步时扮演重要角色。
整体而言,这一阶段是为系统建立清晰架构做准备。在功能实现的同时,也在为模块解耦、数据一致性机制和更清晰的控制流打基础。我们会边开发边调整这些内容,等一切稳定之后再进行结构优化和代码迁移。
game_world_mode.cpp:暂时禁用 PairedEntities
相关代码
我们目前在进行一些清理和重构的准备工作,特别是为接下来的开发打好基础。这一部分的调整主要是为了避免在结构尚未完善的情况下留下不完整或错误的状态,以下是目前做的具体处理和目的说明:
一、暂时清空无用结构以避免干扰
我们有一些不再需要或者暂时不需要参与逻辑处理的字段或结构。当前我们选择将它们“清零”或移除处理逻辑以保持系统简洁,并确保不会因为这些尚未使用的部分导致程序进入错误状态。此操作是临时性的,未来会根据需要逐步引入或重构它们。
二、保持系统在可运行的最小状态
我们做的不是永久删除,而是为了维持代码的基本完整性和防止编译错误,以便下一步开发时可以在一个相对干净且稳定的状态上继续推进。
三、处理返回值与调用一致性问题
在调整函数时,比如某些函数返回值结构发生变化,或者临时不再执行某些逻辑,为了防止中断调用链,我们将返回值填补为默认值或者做了结构兼容性处理。目的是确保已有调用代码在功能缩减的情况下依然可以继续工作,不会出现崩溃或错误。
四、优化对引用索引(Reference Index)的处理逻辑
我们在处理打包引用(pack entity reference)时加入了一个优化判断:
**如果引用的 index 已经是零,说明这个引用已经是无效状态,**我们就不需要再继续对它进行处理或判断是否清除。这个判断可以显著减少不必要的计算。
五、为后续开发预留稳定状态
这部分的改动目的是为了在下一个开发周期(例如计划中的周一开发)开始时,能够在一个清晰、无冲突、逻辑明确的基础上继续推进,不被当前状态混乱干扰思路。
总之,这是一轮必要的“代码净化”过程,确保当前状态不会阻碍后续开发,同时保持结构灵活性和清晰度,是大型系统开发中非常关键的一步。我们将继续围绕引用管理、实体状态同步、brain 结构整合等方面深入推进。
:运行游戏,此时控制器被禁用
我们现在有一类可以理解为“禁用控制器”的功能模块,需要找到一种更干净、整洁的写法来实现它们。为此,我们打算从实际的使用场景出发,先思考理想状态下希望这些控制器的调用和行为是怎样的,然后反向推导出具体实现的代码结构。
具体来说,我们会先设想最理想的使用方式,看看希望怎么调用这些禁用的控制器,代码看起来最简洁、逻辑最清晰。然后再基于这个理想的使用模式,倒推需要怎样的接口设计和内部实现,以便让整体代码既易读又易维护。
这个过程实际上是先设计“调用端”的需求和期望,再回到“实现端”去调整代码和架构,确保二者契合。这样可以避免写出难用或者冗杂的代码,也有助于形成更合理、更模块化的系统结构。
总之,我们的目标是用“从需求倒推实现”的方法,把“禁用控制器”这部分功能写得既干净又高效,同时便于今后扩展和维护。
game_world_mode.cpp:将 Hero 头部控制器逻辑提升到 Brain_Hero
并开始编写其逻辑
我们现在面对的是一段比较乱糟糟的控制器代码,主要涉及“英雄头部”和“英雄身体”相关的逻辑。我们决定先把英雄头部的代码整体提取出来,放进一个叫做“brain hero”的模块里。
在“brain hero”里,有一个“controlled hero”的概念,本质上就是遍历这些被控制的英雄,找到我们需要处理的那个英雄。虽然目前实现上还不是特别高效,比如控制器ID和实际物理控制器ID没有完全对齐,但暂时可以接受,后续可能会优化。
控制器ID会用作脑部ID(brain ID),这在判断和关联控制时比较方便。提取完头部的代码后,我们还会把身体相关的代码也拿出来,准备放到一起。当前对身体部分的具体处理还不明确,但先把代码整理到一起,方便后续清理和改进。
整体思路是先把分散、杂乱的代码模块化,集中管理,然后逐步优化,让控制器和英雄的逻辑更清晰,更易于维护和扩展。
我们现在在处理一段比较混乱的控制器相关代码。首先,我们决定将所有和“hero head”(英雄头部)相关的逻辑整理出来,并将其迁移到 brain_hero
相关的代码模块中。在这个模块中,有一个“controlled hero”(被控制的英雄)的概念,其实就是我们需要找出当前要操作的那个英雄实体。
原本的做法是在所有被控制的英雄中查找当前对应的那个,这其实不是特别高效。理论上,我们完全可以让控制器的 ID 与实际的物理控制器(比如 0、1、2、3)一一对应,这样查找就可以更直接和高效。虽然暂时还没实现这个优化方式,但现阶段我们会简单地将 brain ID
和 test_hero
绑定起来,这样也能满足当前的逻辑需求。
接着,我们还要把“hero body”(英雄身体)相关的代码也提取出来。这部分逻辑其实不需要放在当前的位置,可以与头部逻辑一起整理,放入 brain_hero
模块内。
通过这一系列整理,我们的目标是让控制逻辑更加模块化、结构更加清晰。虽然目前还没有完成对控制器 ID 优化的处理,但当前的重构为后续优化打下了基础,也让代码变得更易维护和扩展。
现在的任务是处理脑控逻辑的部分
现在的任务是处理脑控逻辑的部分:我们拥有一个“brain”(脑控单元),它控制“head”(头部)和“body”(身体)两个部分,而这两个部分并不总是同时存在。因此,我们的逻辑需要适应这种不确定性。
过去,我们使用一种比较原始的“实体”结构去做判断,现在我们不再依赖这个统一的实体概念,而是分别处理 head 和 body。我们会将某些代码限定为“仅作用于 head”,而其他部分则适用于 head 和 body 的共同处理逻辑。例如:
- 某些逻辑仅在 head 存在时运行(这些属于“head 风格”代码);
- 其他逻辑不区分是 head 还是 body,通用执行。
在清理的过程中,为了判断哪些地方还在引用已经废弃的 entity
变量,我们使用编译器来辅助识别,比如直接编译,查看报错提示,定位代码位置。
此外,我们明确了每个 hero 的 brain ID
会与其对应的 brain 保持一致关联,在后续操作中使用统一的 brain ID
来查找对应的控制单元或行为模块。
总之,这一轮重构主要是为了让代码逻辑更清晰、可维护性更高,取消原始实体结构,转为更模块化的“head-body”脑控模型,并逐步清除遗留代码中对旧结构的依赖。
game_sim_region.h:在 brain
中添加 brain_id
我们目前对实体的 ID 做了重复存储,一方面实体自身已经有 ID,另一方面又在哈希表中保留了这个 ID。从逻辑上来说,这是为了加快比较效率,但实质上这更像是一种优化上的尝试,并没有明确证据证明这样做真的带来了性能收益。
所以我们认为这种做法属于过早优化。在没有性能瓶颈数据支持的情况下贸然增加这种冗余结构,并不合理。相反,这可能会引入额外的复杂性与维护成本。当前来看,这种 ID 的重复存储并没有明显优势,因此应当暂时去除这部分逻辑,除非之后通过实测发现它确实对性能有显著提升。
总结来说,我们需要:
- 停止在哈希结构中冗余保存实体 ID;
- 在未来通过性能分析工具验证是否真的存在必要性;
- 在代码设计阶段避免“未验证的优化”主导决策,优先保证结构清晰可维护。
game.h:将 controlled_hero
中的 entity_id
改为 brain_id
我们当前正在处理 handmade.h
中与控制器相关的结构,特别是那些“受控英雄”的部分。在这里,我们使用的是 brain ID
来标识每个控制器关联的实体。
目前逻辑中涉及 brain ID
的访问时出现了问题,比如报错提示 value
不是 brain ID
的成员。这意味着之前我们可能误以为 brain ID
是一种包含值成员的结构,实际上它可能是个索引(index)类型,而非完整对象。
这个问题暴露出两个方面的内容:
-
关于结构定义的误解
brain ID
被当作有.value
的结构使用,但实际可能只是个简单类型,比如整数或者别名类型;- 如果是别名类型,就无法通过
.value
方式访问其中值。
-
关于控制器和大脑 ID 的映射方式
- 当前我们先保留了
controlled heroes
映射到brain ID
的逻辑; - 但实际上我们**很可能可以通过逆向查找(reverse lookup)**直接从实体获取其
brain ID
,从而消除这一冗余结构; - 暂时不进行这一重构是为了避免引入过多新问题,保持已有逻辑的稳定。
- 当前我们先保留了
接下来我们需要注意:
- 确认 brain ID 的实际类型定义,避免错误地调用不存在的成员;
- 审视是否确实需要 controlledHeroes 数组,是否可以更自然地将控制器与实体直接绑定;
- 最终的目标是让控制器的绑定逻辑更自然、更统一,减少冗余结构与多余遍历。
总之,当前我们暂时维持原有结构以确保编译通过,但后续应当逐步重构,利用更加简洁的方式进行控制器与实体的管理。
game_world_mode.cpp:修复编译错误
我们目前正在整理和清理与实体(entity)及其“大脑逻辑”相关的代码,特别是针对实体的“头部(head)”部分的一些调试性逻辑,以及如何合理组织这些代码逻辑结构。
以下是本阶段工作的详细整理和总结:
头部逻辑和调试生成
- 当前处理的是用于调试目的的实体生成逻辑;
- 实体生成的行为仅在其为“头部”存在时才会触发;
- 存在一些变量(如
DT
)是在之后阶段才构造的,所以当前阶段未能立即使用; debug_spawn_code
这部分实际上只是调试用途,不应出现在正式逻辑路径中;- 所有面向
head
的逻辑目前集中处理,如方向、状态构造等。
代码混乱与清理规划
-
目前逻辑比较混乱,头部和身体逻辑混杂;
-
下一步计划是将逻辑按功能分块:
- 同时存在“头”和“身体”的情况;
- 仅存在“头”的情况;
- 仅存在“身体”的情况;
-
这样可以清晰划分不同分支逻辑,避免冗余处理。
删除实体的逻辑处理
- 调用了
DeleteEntity
的函数; - 希望支持:即使实体不存在也可以“尝试”删除,代码仍能正常运行;
- 检查确认已有实现可以支持这一操作;
- 后续逻辑中要开始有意识地检查实体是否已删除,以防止异常。
与 brain ID 的绑定管理
-
目前所有实体都被分配一个
brain ID
; -
代码中出现
brain_id.value
形式,但其实brain_id
不是带成员的结构;- 这会造成访问错误;
- 后续将调整使用方式,统一访问接口;
-
我们也计划在行为分配时更加清晰地区分逻辑入口,不再滥用 ID 判断。
后续改进计划
- 清理
controlled hero
、head delta
、实体方向等相关逻辑; - 抽象出独立的逻辑处理模块,例如行为处理器(行为树/状态机);
- 优化调试代码,不让其干扰主流程;
- 可能引入统一的调度/更新入口,确保头与体逻辑同步一致;
- 考虑实体是否需要完全拆分出“逻辑形态”(大脑)与“物理形态”(头/体)。
这一阶段的重点是清理头部逻辑、精简调试代码、加强删除实体的鲁棒性,并对 brain ID 的使用方式进行规范。整个系统正在逐渐走向结构清晰、功能分离的方向。
game_world_mode.cpp:让 AddPlayer
返回一个 brain_id
我们目前的逻辑结构已经逐渐清晰,代码之间的衔接也变得更加自然。以下是对当前阶段内容的详细总结:
brain ID 与控制器(controller)的绑定机制
- 每次创建一个“大脑”(brain)实例时,会生成一个唯一的
brain ID
; - 控制器随后会附加(attach)到这个
brain ID
上; - 这让控制关系变得更加合理和结构化,避免了原先随意指定控制目标的方式。
玩家(Player)的构建方式更新
-
原本在添加玩家时,我们人为规定“玩家等于头部实体”;
-
现在不再需要这样硬编码;
-
目前引入的是一种新的抽象结构:
- 玩家是一组松散关联的实体(head、body、其他组件等)的集合;
- 这些实体通过共享一个
brain ID
来标识归属同一玩家; - 任何拥有这个标签的实体都将处于玩家的控制之下;
-
因此不再需要人为设置哪个实体是“主控”或“从属”,而是通过 tag(标签)自然地组织。
自组织式的实体归属机制
-
当我们创建新实体时,只要它们标记为与某个
brain ID
相关联,它们就会**自动组合(self-assemble)**成一个整体; -
这种机制更像是一种“自底向上的解析”方式(bottom-up parsing):
- 控制权不再是手动分配;
- 而是实体根据其属性自动决定归属;
-
这不仅让逻辑更具扩展性,也提升了系统的灵活度和模块化程度。
结果与好处
- 控制结构变得解耦、灵活、易于扩展;
- 可以轻松支持复杂结构,例如多人、召唤物、分身等;
- 无需担心在控制流程中遗漏某一部分实体;
- 行为逻辑和控制逻辑之间的边界也更加清晰。
整个结构正在从“手动绑定控制”转变为“自动组织归属”,并通过 brain ID
构建一个统一、模块化的控制接口,为未来功能拓展和逻辑复用提供了坚实基础。
Q&A(问答环节)
目前阶段的工作暂时告一段落,即将进入问答阶段,但整个系统还需要至少再进行一次全面的整理和完善。以下是本阶段的详细总结:
当前工作回顾
- 目前仅完成了“脑(brain)”系统的初步构建;
- 实际的“控制器代码”和“实体行为”的核心内容尚未整理完毕;
- 接下来的工作目标是清理、完善并验证现有架构的合理性。
下一阶段的工作规划(即“Brain Week”)
-
清理控制器代码(controller code)
- 尤其是角色身体部分(body)相关的控制逻辑;
- 目标是将控制逻辑模块化、结构清晰;
- 去除过时、混乱的旧代码。
-
创建并整理专属文件结构
- 将所有“大脑”(brain)相关逻辑抽出,集中放入一个或一组独立的文件中;
- 例如:
brains.h/cpp
或其他按类型分的模块; - 实现不同实体类型的大脑行为划分(如英雄、蛇等)。
-
实现并测试蛇(Snake)的大脑逻辑
- 编写
brain_snake
的具体实现; - 将其接入整体系统并实际运行测试;
- 验证结构是否支持非玩家角色或 AI 实体。
- 编写
-
评估整个系统的设计是否合理
- 如果新设计让控制逻辑更加清晰、行为更易定义且便于扩展,那么这个架构就是成功的;
- 如果发现结构臃肿或处理麻烦,则需要调整设计理念或架构。
验证机制
- 构建并测试蛇的行为是检验整个大脑控制架构好坏的关键步骤;
- 成功控制非玩家实体并进行行为测试将作为架构有效性的标志。
我好像错过了有关蛇的部分,那是什么?
我们在讨论“snake”的时候,其实是以它作为一种实验性实体,用于验证新的大脑控制架构是否具备良好的扩展性与表现能力。以下是对其核心思路的详细总结:
为什么选择“蛇(snake)”?
-
具备多个组成部分:蛇由多个部分组成,例如“头部”、“多个身体节段”、“尾巴”等,天然适合测试如何用多个实体协同工作,组合为一个整体。
-
非固定结构:不同于英雄角色(head+body 的固定组合),蛇的结构可以是动态或不规则的,长度可变、形态灵活,更贴近真实场景中的复杂对象。
-
行为多样性:蛇不仅可以实现“跟随前一个身体节段”的基础行为,还可以设计为具备全局思考能力的大脑,例如:
- 控制尾巴甩动;
- 做出转向、攻击等统一行为;
- 在面对障碍物或目标时,做出整体决策。
“蛇”在大脑系统中的实验意义
-
验证控制架构的通用性与扩展性:
- 不再只是英雄这种标准角色;
- 看看是否可以处理具有更复杂逻辑与结构的敌人单位。
-
测试实体聚合的动态控制能力:
- 各身体部分是独立实体;
- 是否能被大脑统一协调为“一个整体”运行;
- 是否可以实现数据共享与状态传递,例如方向、速度、目标感知等。
-
避免传统设计的局限性:
- 传统做法常常是“每个身体部分跟随前一个部分”;
- 在新架构中尝试采用“集体智能”的形式,例如蛇的大脑统一计算路径,然后各部分根据全局指令行动。
蛇的控制模型设想
- 每个部分都是一个独立实体(Entity);
- 每个实体都拥有“brain_id”字段,用于标识它属于哪个大脑(例如同一条蛇);
- 蛇的“大脑”是一个控制器,拥有对整个结构的理解与控制;
- 行为是整体驱动的,而非节段间传递的。
使用蛇进行验证的优点
- 实现简单:蛇的基础行为逻辑清晰,容易快速搭建测试;
- 反馈明确:通过观察是否能控制多个节段协调运动、改变方向、攻击等,可以迅速得出当前架构是否可行;
- 容易扩展:若验证成功,可以进一步扩展为其他聚合式敌人,例如多头龙、连体机器人等。
总之,蛇并不仅是一个普通的敌人,而是一个非常理想的试验对象,它能够帮助我们以简单的方式验证复杂的控制逻辑是否能够支撑多实体组合、统一思考、集体行为的需求。这个测试将在后续的“brain week”阶段推进,成为新架构设计有效性的关键判断标准之一。
其实是个马陆(千足虫)
我们在这里进一步澄清了关于“蛇(snake)”的比喻其实更贴近于“蜈蚣(centipede)”或“千足虫(millipede)”的概念,尤其是像经典游戏《Centipede》中那种生物的行为方式。以下是详细的中文总结:
比喻更准确是“蜈蚣”而非“蛇”
- 虽然最初用“蛇”作为比喻,但实际设想的行为和结构,更像是蜈蚣或千足虫那种具有大量身体段、线性排列并具有一定自主行为的生物。
- 特别是参照**游戏《Centipede》**中的敌人设定,它们虽然看似是一个整体,其实每一节也有相对独立性,这与当前的架构理念非常契合。
为什么更像《Centipede》里的生物?
- 每一节都像一个“实体”,但通过逻辑归属到一个统一的大脑控制单元;
- 整体拥有一种分布式又统一的控制逻辑;
- 可以整体响应外部刺激,如转向、避障、攻击等;
- 每个身体部分可独立运行,但最终决策来源于整个“生物体”的统一行为。
游戏结构测试目标
-
创建类似《Centipede》中的长生物,用来测试系统对多实体一脑控制的适应性;
-
验证系统是否能处理:
- 多个实体之间的同步逻辑;
- 分布式位置与速度计算;
- 连续移动与协同行为;
-
更进一步的话,甚至可以测试实体的动态增减,例如被攻击后节段减少,或生长时添加新段。
关于项目持续时间的回答
- 目标是尽可能完成基础架构的完善;
- 尤其是这个“脑—体—控制器”架构,需要经过多个类型实体(如英雄、蜈蚣)测试,来确保其适用于更广泛的游戏对象;
- 这将持续到至少完成几个核心测试实体(如蜈蚣、蛇等)之后,并验证它们能正常运行,架构设计合理;
- 并没有一个明确的终点时间,而是取决于架构验证的完整性与实用性是否达到目标。
你希望这个项目持续多久?结束后你会开始做不同的游戏吗?
我们明确表示,这将是我们在直播中制作的唯一一款游戏,并没有计划在此项目完成后再制作其他游戏,原因如下:
项目规模和计划
- 这个项目预计耗时 600 天;
- 这里的“天”并非指自然日,而是按每次直播约 一小时计算,因此是 600 小时;
- 如果换算成标准全职工作时间(每周 40 小时),等价于 约 15 周(大约 3 到 4 个月);
- 对于一款从零开始、不依赖现成游戏引擎、完全自建架构的游戏来说,600 小时并不算长;
- 如果是在成熟引擎(例如 Unity、Unreal)里制作,只需设置现成的实体标志位,那可以很快;
- 但我们的目标是自底向上开发,包括底层系统、渲染、控制逻辑等,因而时间成本更高。
不打算开发第二款游戏的原因
- 由于投入巨大、复杂度高,仅仅完成这一个项目就已经是非常庞大的任务;
- 整个开发周期跨越多年,并通过持续直播推进,不具备短期快速复用的条件;
- 再开发一款游戏意味着类似的大量投入,而当前这个项目已被视为长期计划的核心工作;
- 因此我们并未考虑进行下一款游戏,而是专注于将这一款做到尽善尽美。
关于游戏内容量的考量
- 游戏内容可能会超出最初设想的 600 小时;
- 特别是游戏玩法部分还未全部明确,如果内容过于丰富,开发周期可能会延长;
- 但我们将持续评估与推进,视情况动态调整。
总结
我们专注于这一个游戏项目,投入大量精力构建从底层到表现层的所有系统,时间估算为 600 小时(直播小时计),这并不多,考虑到我们没有使用现成引擎。这款游戏将是我们直播期间唯一开发的游戏,预计涵盖极其丰富的内容,未来不会再做第二款。
关于每个网格只能有一个实体的检查,如果你刚离开某个格子,一个怪物正好落在那里,现在这是个二元判定,你打算加入一点模糊处理吗?比如刚踩离一个格子但怪物还未落地。
我们讨论了“每个格子只允许一个实体”的设计限制,以及在高速动作交错时可能出现的边界冲突问题。具体整理如下:
格子占用冲突的背景
目前设计中,每个格子只能容纳一个实体。这种“格子唯一性”逻辑导致了潜在问题,例如:
- 玩家刚刚离开一个格子,怪物跳入;
- 玩家刚准备进入一个格子,怪物才刚跳出;
- 玩家和怪物几乎在同一帧内交叉走位。
当前处理思路
-
怪物不会跳到玩家格子
怪物不会“主动”跳到玩家所在的格子上,除非是在进行攻击。而攻击动作本身并不意味着怪物要“站”到玩家所在格子上,它仍可以保留当前位置来发起攻击。 -
关键冲突点是“交叉进入”
真正要处理的是双方几乎同时进出同一格子的情况。例如怪物刚跳出一个格子,玩家也正打算跳进去。 -
跳跃逻辑为:“中心线判定”
目前的解决方案是假设在移动过程中,角色在完全越过格子的中心线前仍然视为处于原始格子。当某个实体越过中心线时,才算正式进入新格子。因此:- 玩家若想跳入某格,前提是怪物必须已经完全跳出(即中心线后的下一步);
- 若怪物未能“及时清空”目标格子,则玩家无法跳入;
- 反之亦然。
-
动作判定策略可能是“最早清空者优先”
怪物若能尽早腾出格子,就优先做出移动判断;否则其格子仍视为“占用中”。
游戏性上的取舍
- 承认这是设计层面不可避免的“生硬感”之一;
- 有可能会出现略显卡顿、不那么丝滑的体验;
- 但游戏设计本身要求格子唯一占用,这是根本机制,必须优先保障。
总的来说,这是游戏底层逻辑带来的限制之一。为保障战斗判定的明确性,将优先保持“一个格子一个实体”的规则,即使可能会牺牲一部分动态流畅感。
有天我想问问你,如何思考程序中数据的表示。我理解结构体、枚举、数组这些东西,但是什么时候用哪个,有什么规律?
我们谈论了在程序设计中如何选择和使用不同架构的思考方式,具体内容如下:
关于程序设计架构选择的思考
-
没有绝对规则指导什么时候用哪种架构
程序设计不像数学公式,有明确步骤和唯一答案。非常简单的程序可能有标准答案,但复杂程序设计远没有固定模式。 -
经验和反复实践是关键
只有经过大量的实践、尝试不同的架构风格,才能逐渐培养出判断和选择的能力。通过实践,可以不断探索各种方案,了解每种方案的优劣。 -
架构选择是一个不断试验和迭代的过程
当面对不熟悉的问题或想尝试新的做法时,需要对多种方案进行排列组合和测试,而不是直接给出答案。
通过比较各方案的代码复杂度、bug数量、可维护性等指标,逐渐总结经验。 -
程序设计更像是一种艺术或运动
编程设计没有“一招致胜”的法则,更类似于运动或艺术,需要不断训练和反复调整。没有简单的规则或公式能够直接套用。 -
观察和学习他人的设计过程也非常重要
看别人的设计思路和实践能帮助提高自己的架构能力,理解不同架构如何影响代码表现和维护。 -
导师或教练的帮助能加速成长
就像运动员有教练指导动作一样,找一个有经验的人点评和指导架构设计过程,指出潜在的错误和改进方向,会大大提升学习效率。 -
总结和记录是提升的辅助工具
记录各种架构尝试的成败原因、好坏之处,有助于内化这些经验,形成自己的设计直觉和方法论。
总结
- 编程架构选择没有现成的规则或模板,更多靠不断的实践、试错和总结。
- 通过不断尝试和观察,培养判断不同架构适用场景的能力。
- 这是一种长期积累的能力,不断演进,而不是一次性完成的任务。
- 有导师指导和详细记录都能帮助更快提升架构设计的水平。
这种对架构设计的理解和思考方式,能帮助我们面对复杂问题时不迷失方向,持续提升设计质量。
这个系统的目标是不是既支持可以合并变形的实体组(比如伪装的怪物),又支持这些实体脱离后能单独行动(比如被死灵法师复活的断臂)?
我们讨论了实体系统如何支持两种复杂情况:
实体系统的设计目标和能力
-
支持动态变化的实体组合
实体系统要能够支持一组实体从一种形态变换成另一种,比如一个怪物能够模仿其他对象,动态变化其组成部分。 -
支持实体的分离与独立行动
系统还要允许实体在脱离原有组合后,可以单独行动,比如死灵法师能复活被撕下的手臂,这个手臂作为独立实体继续活动。 -
跨实体算法的实现
通常每个实体自身都有“思考”能力,但要能写出跨多个实体协同工作的算法非常重要,之前的设计难以实现这一点。 -
引入“脑”层(Brain Layer)简化多实体协作逻辑
新的设计中,通过引入脑层概念,可以编写跨实体的代码,使得多实体间的协作逻辑更清晰、简单,避免复杂交叉混乱。 -
系统的简洁性和扩展性
现有的MV系统结构简单,未来可以更方便地添加新的功能,比如给某个实体增加“诅咒”效果或其它特殊状态,这种扩展将变得容易。 -
解决之前设计限制
之前的实体系统限制了跨多个实体统一思考的能力,难以干净利落地实现多实体间复杂交互,这是推动新设计的重要原因。
总结
- 实体系统不仅要支持单个实体的行为,也必须支持实体群体动态组成和拆分后的独立行动。
- 通过引入“脑”层,实现跨实体的统一“思考”和行为控制,提高代码的整洁度和可维护性。
- 设计追求简单和可扩展,方便未来快速增加复杂特性。
这使得复杂的游戏实体管理更加灵活且高效。
关于 UML,你在工作中会经常使用吗?尤其在游戏开发行业?
在游戏行业工作中,实际上从未见过有人画过UML图,无论在什么情况下都没有见过。对UML图的使用几乎为零,说明在实际开发过程中,UML并不是常用的工具或方法。
好答案,我明白了,多做就能知道什么有效,靠经验。谢谢!
我们的理解是,编程架构没有固定的规则和唯一的答案,必须通过大量实践去摸索和积累经验。不断尝试不同的方法,观察效果,总结优缺点,才能逐渐找到适合自己的解决方案。只有不断动手实践,反复调整,才能提升能力,找到比较合适的架构设计方式。简单来说,就是多做多练,通过经验积累来理解什么方法行得通,什么不行。
你考虑过用 Vulkan 吗?它对硬件提供了非常底层的访问。
我们之前研究过Vulkan的设计,但不喜欢它的设计理念,因此几乎可以确定不会在项目中使用它。除非出现特殊情况必须用到,否则不会选择Vulkan作为方案。