游戏引擎学习第285天:“Traversables 的事务性占用”

发布于:2025-05-17 ⋅ 阅读:(15) ⋅ 点赞:(0)

回顾并为当天的工作做准备

我们有一个关于玩家移动的概念,玩家可以在点之间移动,而且当这些点移动时,玩家会随之移动。现在这个部分基本上已经在工作了。我们本来想实现的一个功能是:当玩家移动到某个点时,这个点能“知道”玩家是占据它的人,如果有其他人想移动到同一个点,则不允许这样做。为此,我们希望建立一个简单的事务系统,来管理点的占用情况,方便测试和控制。除此之外,暂时不处理房间中带有斜角或坡度的复杂情况,所以打算关闭房间斜坡这一特性,先专注于点的事务性占用管理。

game_world_mode.cpp:禁用坡度功能

把那条线去掉后,构建物体的Z轴偏移就变成了零,这正是我们想要的效果。这样房间就是一个平坦的空间,可以正常跳跃。场景里还有一个小角色,如果时间点合适,可以跳到这个移动的角色身上,测试时保留这个功能没有坏处。

接下来,我们要重新整理“站在某物上”的概念。为了实现这个功能,我们需要一个登记机制,也就是说,要有一个系统能够记录谁站在哪个点上。这样模拟区域(sim region)才能知道某个点已经被占据,不允许其他人去那里。如果那个点是空的,系统就可以告诉玩家那里是可去的。

为此,我们需要写一些代码来跟踪这个状态。实现方式有几种,比较合理的是直接在实体的解包(unpack)过程中处理,因为解包时必须知道这个实体对应的是哪个可通行点(traversable)。在这里直接存储哪个实体占据了该点,是最简单直接的做法。于是接下来就要看看如何具体实现这个方案。
在这里插入图片描述

game_entity.h:在entity_traversable_point中添加了一个指向实体的指针 *StandingOn

在实体结构中已经有了可通行点(traversable point)的概念,它存在于碰撞体积(collision volume)内。如果想的话,可以把某个实体和这个可通行点关联起来,比如说给这个点一个实体引用,表示某个实体正站在这里。

其实不需要把这个信息打包保存下来,因为实体本身在运行时就知道自己站在哪个点上,所以只需存储实体指针即可。这个指针只需要在模拟区域(sim region)内有效,保存和加载数据时可以忽略它。打包(pack)时不需要特殊处理,指针可以不写入保存的数据;解包(unpack)时再更新这个指针。

具体来说,在世界打包操作时,打包实体到数据块(chunk)、打包可通行引用、打包实体引用时,都不需要额外操作,因为这个指针最终会被丢弃。解包时,在加载可通行点数据的第二遍处理中(connect entity pointers 阶段),会进行指针的更新和关联工作。

总体思路是在创建和加载实体时,合理处理这些指针的赋值和更新,使得实体和它所站的可通行点能够正确对应。
在这里插入图片描述

game_sim_region.cpp:让LoadTraversableReference在实体有指针时写入实体的碰撞体积

在加载实体引用时,不仅仅是简单地加载实体引用本身,而是要在加载完之后,检查加载到的实体是否有效。如果有效,就需要更新该实体的碰撞体积中的可通行点信息,明确告诉它“这个可通行点现在被谁占据了”,也就是说,让这个点知道当前站在上面的是哪个实体。

为了做到这一点,在加载可通行引用时,需要知道这个引用的来源实体是谁,把这个信息在解包过程中保存下来。然后在解包时,将这个信息写入对应的实体,使得实体的碰撞体积中的可通行点能够正确记录当前站立者。

具体实现时,加载时知道当前处理的实体,因此可以正确更新指针和状态。当加载完成后,相关指针就会被正确更新,实体和它所站的可通行点之间的关系得以正确维护。这样才能保证模拟区域内部关于“谁站在哪个点上”的信息是准确的。
在这里插入图片描述

game_world_mode.cpp:如果traversable点被占用,则为其着色

为了观察角色在不同可通行点上跳跃时的情况,我们打算根据这些点是否有实体指针关联来给它们着色,从而直观地看到每个点的状态。刚开始颜色显示可能会不准确,因为我们还没有清除之前的状态,但至少可以通过颜色的变化来追踪代码的运行效果。

具体做法是在绘制可通行点时,使用绘制矩形轮廓的方法,但注意到这些点本身没有碰撞器,所以绘制时要特别处理。在绘制过程中,会检查该可通行点的颜色状态,进而反映是否有实体“占据”它。这样,通过颜色就可以清楚地看到哪些点被占用,哪些点是空闲的,有助于调试和后续开发。

先把逻辑和绘图分开一下之前没做这个

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行游戏,发现traversables已经带有新的颜色

我们原本预期这些可通行点的状态在运行时会立即发生变化,但实际观察后发现它们并没有如预期那样立即更新,原因在于这些碰撞点结构是共享的。这意味着多个实体实际上引用的是同一组数据,因此在一个地方修改并不会体现为独立的、个体化的状态变化。这显然不是我们想要的行为,因为我们期望每个实体能独立维护自己的状态。

这个问题引发了一个关键的设计思考:是否还要继续让这些结构在多个实体之间共享,还是应当将它们“展开”成为实体自身的一部分,也就是说,把数据写入每个实体自身中,而不再是共用一份。这是一个不太容易决定的问题。

从资源使用角度来说,如果这些数据非常庞大,比如每个实体要占用 64KB 的碰撞数据,那么共享是显然更合理的。但当前的情况是这些数据体积其实不大,因此是否共享并不是明显的优劣之分。

我们进一步分析当前的结构发现,这些碰撞体其实就是某种“碰撞组”,大多数情况下它们都是类似的,例如很多地面砖块的可通行点都设在同一个位置(如 0,0,0),所以共享是合理的。但考虑到我们设计的是一个表达能力极强、可配置性很高、偏向“有机生长”的实体系统,最终的倾向是——每个实体应该有自己独立的 traversable(可通行)点,而不是共享的。

这样做的好处是:每个实体的可通行点可以随意变动、被修改,并且不会影响其他实体的行为。这种灵活性更适合我们当前正在构建的系统风格。至于存储开销问题,尽管每个点可能需要 8 或 12 个字节的空间,也许我们可以通过建立一个索引表的方式,将它们压缩为一个字节来引用,从而节省内存。

总之,在共享结构和独立结构之间,我们更倾向于选择让 traversable points 成为每个实体自身的组成部分,以支持更强的灵活性和表达力,尽管这会增加一定的内存使用和管理复杂性。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game_entity.h:将Traversable设为实体的一个属性

我们决定不再将可通行点(traversable points)作为碰撞组(collision group)的一部分。相反,我们将其作为实体(entity)本身的属性进行管理。这么做的原因是我们希望每个实体都可以拥有自己的可通行点,并直接附带占用者(occupier)信息,这样能避免共享结构带来的混乱,并使系统的表达能力更强。

为了快速实现这个系统,我们初步采用了硬编码的方式,为每个实体设定一个固定数量的可通行点,使现有代码能继续运行。不过,我们意识到,这也许是一个合适的时机,顺势将可通行点设计为一个灵活长度的数组。这样,不同实体就可以根据需要拥有不同数量的可通行点,从而实现更复杂的交互逻辑与空间结构。

具体实现上,我们不再通过碰撞组来访问可通行点数据,而是直接在实体结构中维护这些数据,包括可通行点的位置和对应的占用者指针。这也简化了很多代码逻辑,因为不再需要从碰撞体组中间接地查找数据,改为直接从实体结构中读取和写入。

在处理过程中,我们还需要确保所有创建实体的代码路径都被更新,以正确初始化其可通行点。例如,在构造地面区域时,我们将可通行点的位置设定为目标位置,同时将其占用者设置为空。这样,一旦实体被放置在该点上,占用者就可以被正确设置和跟踪。

测试运行时我们发现了一个有趣的现象:由于初始只在一个特殊的“上下移动平台”上添加了可通行点数据,导致玩家立即跳到了那个唯一拥有可通行点的实体上。尽管这不是预期效果,但也印证了我们的逻辑修改生效了。为了解决这个问题,我们把相同的初始化逻辑也添加到其他普通地面实体中,使整个系统恢复到与之前一致的运行状态。

总的来说,我们已经完成了将可通行点从共享结构中独立出来,并与实体自身绑定的改造。这为后续实现更复杂的占用控制逻辑和实体交互打下了坚实基础,也提升了整个实体系统的灵活性和可配置性。
在这里插入图片描述

怎么占用的没变颜色呢

在这里插入图片描述

在这里插入图片描述

EntityType_Floor 没设置颜色的原因
让共用一个case
case EntityType_Floor:
case EntityType_FloatyThingForNow:

运行游戏,控制Pacman移动经过traversable点

我们在移动过程中可以观察到,每当实体跳到一个新的可通行点上时,该点的占用者指针就会被设置,表明它被占用了。这部分机制已经运行正常,但目前仍存在几个关键问题需要解决。

首先,占用者指针一旦设置,就永远不会被清除。当前的实现逻辑仅在解包(unpack)阶段设置占用者指针,也就是说,只在帧与帧之间的状态重建时,根据上一帧的位置将当前实体标记为占用了某个可通行点。这显然是不够的,因为我们并未在实际移动实体的逻辑中更新或清除这些占用状态。这意味着,一旦某个可通行点被标记为已占用,就不会因为实体移动离开而恢复为空,这会导致错误的占用状态残留。

其次,目前系统还无法阻止多个实体同时占用同一个可通行点。这是我们接下来需要重点解决的问题。为此,我们需要引入一个新的逻辑概念:在尝试占用某个可通行点之前,必须检查它是否已经被其他实体占用了。只有当目标点为空时,才能完成占用;否则,应该拒绝此次移动。

接下来的工作将围绕这一目标展开。我们将深入检查处理实体移动的代码路径,增加以下几个关键步骤:

  1. 在每次尝试移动之前,查询目标可通行点是否已经有占用者
  2. 如果没有占用者,允许移动,并更新占用者指针为当前实体
  3. 如果已经有其他实体占用,则拒绝移动请求
  4. 每次实体从原位置离开后,要清除原可通行点上的占用者指针

实现这些步骤之后,我们才能真正建立起一个有效的**“事务性占用”系统**:每个可通行点最多只能被一个实体占用,系统会自动管理占用与释放,避免位置冲突,也为后续的角色交互、碰撞管理、AI移动控制等提供了必要的基础逻辑支持。

game_world_mode.cpp:在跳转到traversable点之前检查该点是否可用

当前的代码体系中,还没有建立一个良好的权限判断机制。现在的做法是在移动实体时,直接设置目标位置的占用者,而没有经过任何检查与验证。也就是说,我们默认目标位置是空的,这在多实体交互或者竞争式空间占用场景中是明显不合适的。

具体来看,在尝试让某个实体移动到某个可通行点时,代码中直接将目标可通行点标记为该实体正在占用,这种“盲写式”的处理方式正是当前系统的核心问题所在。为了解决这个问题,我们计划引入一个事务式占用系统(transactional occupy),它可以安全地决定是否允许某个实体占据指定的可通行点。

我们要做的是构建一个新的函数接口,例如叫作 TransactionalOccupy,用来表达这样一个意图:“我(某个实体)希望将自己某个槽位中的位置设置为此目标可通行点,请问可以吗?” 这个函数会返回一个结果,说明此次请求是否被允许。

如果请求被批准(目标位置未被占用),我们就可以开始跳跃动作;如果请求失败(目标已被占用),我们就不执行移动。此外,为了保持逻辑清晰,我们打算使用两种不同的失败路径来区分

  1. 一种情况是“我们并没有尝试移动”;
  2. 另一种情况是“我们试图移动,但失败了”。

这个区分很重要,因为控制逻辑(如AI或玩家输入处理)可能需要根据失败类型做出不同反应。举例来说:

  • 如果我们从未尝试移动(因为玩家没有输入方向),那控制器不需要做任何事;
  • 如果我们尝试了但目标点被占用,那么控制器可能会选择等待、重试、改变策略或寻找替代路径。

为此,我们打算在初步实现中将这两种情况明确分开处理,以便后续扩展控制逻辑。

综上,事务式占用机制的核心作用有三点:

  • 确保空间的互斥使用(一个可通行点只能被一个实体占据);
  • 为控制器提供明确的反馈路径(尝试失败 vs 未尝试);
  • 为未来行为系统和AI设计打好基础(如拥挤检测、阻塞状态响应等)。

这将是整个移动系统从被动单向“写入”,转向主动、安全、多实体并发协同的关键一步。
在这里插入图片描述

game_sim_region.cpp:引入TransactionalOccupy和GetTraversable函数

目前正在实现一个“事务式占用”机制,用于判断实体是否可以占据某个目标可通行点,并安全地进行占用与释放操作。

首先,我们在模拟区域(SimRegion)中创建了一个新的操作接口 TransactionalOccupy,其目的是在实体尝试移动到目标位置之前进行判断和处理。如果成功,则更新占用信息;如果失败,则不允许移动。

步骤与实现细节:

  1. 准备访问接口
    为了方便访问实体的可通行点数据,我们实现了一个 GetTraversable 函数,允许通过实体指针和下标来获取对应的可通行点。这包括处理边界检查(如断言)以避免越界访问,确保数据安全。

  2. 事务式占用逻辑
    TransactionalOccupy 函数中,传入两个参数:

    • destRef:当前实体原本所在的位置引用(destination)
    • desiredRef:当前实体想要移动到的位置引用(desired)

    接着,我们根据 desiredRef 获取目标的 traversable 点 desired,检查其是否已被占用。

    • 如果目标未被占用:允许占用,设置其占用者为当前实体。
    • 如果目标已被占用:不允许占用,操作失败。
  3. 原位置释放占用
    如果允许占用新位置,还要同步地释放旧位置的占用信息:

    • 通过 destRef 获取当前占用点 dest
    • 将其占用者设置为无(null),表示当前位置被释放。

    然后将 destRef 更新为 desiredRef,表示实体现在正式处于新位置。

  4. 默认返回值处理
    初始实现中忘记返回成功标志,因此即便操作成功,也被认为失败,导致移动逻辑无法执行。已修正为在成功设置占用时返回 true

  5. 非初始化引用处理
    特别注意,destRef 可能是无效或未初始化的。在这种情况下,GetTraversable 应该返回一个空值或零结构,防止非法操作。实现中已添加条件分支判断是否为有效实体指针,仅在有效时才执行访问。

  6. 位置计算时的安全性考虑
    对于获取可通行点后的位置偏移计算也添加了保护机制:如果 GetTraversable 失败,就保留当前位置;否则才会基于获取到的偏移进行实际位置更新,避免因无效引用导致坐标错误。

  7. 扩展与复用
    同样对其他涉及位置偏移的逻辑(如空间计算)也做了类似修改,统一使用 GetTraversable 接口,确保逻辑一致性与安全性。


总结:

这一阶段完成了从“直接写入占用信息”到“事务式、安全验证”的过渡,实现了实体之间空间互斥的基础设施。事务式占用机制:

  • 避免多个实体占据同一位置
  • 清晰管理占用和释放逻辑
  • 为未来AI路径判断、阻挡检测提供了清晰入口
  • 提高系统健壮性,避免非法访问和状态错误

接下来可进一步考虑将此机制融入移动控制器、导航系统中,实现完整的路径判断和动态调度能力。
在这里插入图片描述

在这里插入图片描述

跳过程有问题

在这里插入图片描述

运行游戏,发现几个问题

我们现在遇到了一些问题,尤其是关于“占用”的定义和行为上的混淆。


第一个问题:头部和身体的占用状态不明确

我们目前尚未清楚区分实体“头部”和“身体”在占用格子上的行为。从观察结果来看,好像两者都在尝试占用格子,这导致了逻辑上的混乱。

具体来说:

  • 英雄角色的“头部”似乎没有设置任何占用信息,但又出现在占用逻辑中。
  • 这说明头部可能在逻辑上是“经过”某格子,而不是“站在”其上。
  • 我们现在的系统没有明确地区分“正在移动中(Moving To)”和“实际站立中(Standing On)”的状态,这就使得“占用”逻辑很模糊。

第二个问题:事务式占用没有区分移动路径与最终落点

目前的 TransactionalOccupy 并没有处理“移动中间状态”的特殊性。在角色“跳跃”或者从一个格子移动到另一个格子的过程中,我们处于两个格子之间,这种状态下是否应该算作“占用”某个格子是不明确的。

问题在于:

  • 当前只要进行移动就会立即设定目标格子的占用者。
  • 然而,在实际表现中,实体还处于两个格子之间。
  • 原来的格子是否应该立刻释放占用?新的格子是否应立刻占用?这些逻辑尚未理清。
  • 没有精细区分“开始移动”、“占据中”、“完成移动”三种阶段。

第三个问题:解包操作(Unpack)导致了错误的占用状态

目前在 LoadEntityReferenceLoadTraversableReference 等解包逻辑中,也对占用字段进行了操作,但这实际上是不合理的。

我们认为:

  • 占用逻辑应只在真正“站立”时触发,而不是在数据被读取或解包时。
  • 解包的目的是还原状态,而不是建立新的占用关系。
  • 如果在每一帧解包中都更新占用者,就会导致原本已移动走的实体仍然被视为占用目标格子,造成逻辑冲突。

下一步计划

我们必须重新审视整个“占用”系统的模型,并划清如下几个关键概念:

  1. 正在移动中(Moving):实体处于动画过程中的过渡状态,不应修改占用状态。
  2. 实际站立中(Standing):实体处于稳定位置,应该唯一地占据一个可通行点。
  3. 解包状态恢复(Unpack):用于还原数据,不应引发副作用,如设定占用信息。

为了解决这些问题,我们需要:

  • 为“占用”状态设置明确的触发时机,只在完成移动并落定的时刻才设置。
  • 抽象出“当前站立位置”这一角色的状态字段,仅该位置用于处理占用逻辑。
  • 修改 TransactionalOccupy,增加状态判断,确保逻辑只对实际占据格子进行处理。
  • 重构解包逻辑,避免在数据加载阶段意外更改占用状态。

总结

目前的占用机制尚不完善,核心问题在于:

  • 没有区分“移动中”与“占用中”;
  • “头部”是否占用与逻辑模型不一致;
  • 解包操作引发副作用;
  • “占用”机制需要被更精确地建模为一个状态机。

后续我们需要图示与状态划分辅助理清这些逻辑,并对移动系统与控制器进行联动更新,以保证状态的正确传播和处理。

Blackboard:从Standing On状态切换到Moving To状态

我们当前面临的核心问题是关于“跳跃(hop)”过程中格子占用的时机控制。


问题描述

假设角色从格子 A 跳跃到格子 B,这个过程中涉及两个关键状态:

  1. Standing On(站在):表示当前实际站立并占用的格子;
  2. Moving To(移动至):表示移动目标格子,即接下来要跳到的地方。

目前系统存在的问题在于,我们可能同时占用了两个格子(A 和 B),但没有在合适的时间释放旧格子(A),也没有很好地管理何时接管新格子(B)。


占用时序图分析

如果用时间轴来表示占用关系:

  • 初始状态:我们占用格子 A;
  • 跳跃过程中:要么同时占用 A 和 B,要么必须在合适的时间释放 A 并接管 B;
  • 最终状态:只占用格子 B。
正确情况:
  • A 和 B 占用期间无空档,可以是:

    • 重叠:在占用 A 的同时已经占用了 B;
    • 无缝切换:恰好在释放 A 的同时接管 B。
错误情况:
  • A 和 B 之间存在空窗期,即两个格子在某个时间都未被占用:

    • 会出现其他实体可能在这段时间内“插队”占用 A 或 B;
    • 造成跳跃失败或冲突,逻辑上不可接受。

当前逻辑的问题

目前我们在跳跃开始时直接占用了两个格子(A 和 B),而没有释放 A,也没有延迟接管 B 的机制:

  • 没有一个明确的机制在跳跃落地时释放 A;
  • 在跳跃过程中,系统无法区分“即将离开”与“尚未占用”的中间态;
  • 实际造成了“冗余占用”和“占用状态滞后”。

可行方案探讨

我们必须重新设计跳跃过程中的占用逻辑。这里有几种思路:

方案一:只占用“当前站立”格子(A),不占用“目标格子”(B)
  • 在跳跃过程中,我们只保持对 A 的占用;
  • 等到跳跃真正完成(落地帧),再尝试事务式占用 B;
  • 如果占用失败(例如 B 被别人先占了),跳跃失败或中断。

优点:逻辑清晰,始终只占一个格子
缺点:有可能在空中时 B 被别人抢先占用,导致落地失败,影响体验


方案二:在跳跃开始时占用 B,同时延迟释放 A(即暂时占用两个格子)
  • 在跳跃开始帧,同时占用 A 和 B;
  • 跳跃结束帧,释放 A,仅保留 B 的占用;
  • 保证整个跳跃过程中不会有人“插入”到 A 或 B 中间。

优点:跳跃过程更安全无冲突
缺点:短时间内两个格子被占用,可能导致空间利用率下降


关键原则:
  • 不能出现“中间无人占用”的状态,即 A 释放后 B 未占用;
  • 至少占用一个格子,最多同时占用两个(过渡态)
  • 释放和接管必须是事务式:要么全部成功,要么都不动。

下一步实施方向

  1. 明确跳跃起始与落地的帧;
  2. TransactionalOccupy 改为能够表达“延迟释放”和“预占用”的模式;
  3. 在跳跃起始时占用 A + 事务尝试占用 B;
  4. 如果成功,则标记为“双占用中”,跳跃结束帧释放 A;
  5. 如果事务占用 B 失败,则取消跳跃或重新规划。

总结

我们的问题本质上是“如何保证跳跃过程中占用状态的连贯性”。当前的方式占用了两个格子但未及时释放,导致状态残留。而真正合理的处理是以时间为核心控制点,事务化操作跳跃起点与终点的占用权转移,避免空窗、冲突与资源竞争。接下来需要在 entity 逻辑中重构跳跃过程的状态切换点。

game_entity.h和game_world_mode.cpp:假设占用1个格子的实体一次只占用一个格子

我们决定调整实体在跳跃过程中对格子的占用逻辑,核心目标是整个跳跃过程中任意时刻只占用一个格子,从而避免复杂的双格占用逻辑和可能引发的资源冲突问题。


核心假设

我们现在设定:
实体在任何时刻只占用一个格子(tile),也就是说:

  • 如果是站立状态,那就只占用“站在”的格子;
  • 如果是移动过程中,那就只占用“目标”格子;
  • 或者某些特殊实体(如 Boss)天然拥有多个格子的占用面积,这种情况就视为实体的“体积”更大,这些情况独立处理。

我们不打算让普通实体在跳跃期间同时占用“来源”和“目标”格子


数据结构调整

我们引入一个字段:occupying,表示当前实体真正占据的那个格子。同时添加一个 came_from 字段,用于记录上一个来源格子。

这两个字段将配合使用:

  • occupying:当前正在占据的格子;
  • came_from:跳跃中记录的来源位置,但不占用。

Pack 阶段处理逻辑

在数据打包阶段,我们有两个相关字段:

  • standing_on(站立的格子);
  • moving_to(即将移动的目标格子);

我们要明确只有 occupying 字段真正改变格子的“被占用”状态,而 came_from 只是一个辅助记录,不参与实际的占用逻辑。


行为逻辑改变

以前的逻辑中,在跳跃过程里实体可能会:

  • 同时“站在”A;
  • 同时“移动到”B;
  • 并且 A 和 B 都会被标记为“被占用”。

现在的逻辑调整为:

  1. 当跳跃开始时:

    • 仅将目标格子设为 occupying
    • 来源格子记录为 came_from,但不再保留“占用”状态
  2. 跳跃结束后:

    • 实体的位置更新完成;
    • 只保留对目标格子的占用;
    • came_from 无需再使用,可清空或重置。

适应场景扩展

这个方式的一个好处是,它能够支持更通用的情况,例如:

  • 支持大体积实体只在每一帧更新中明确自己占用的多个格子;
  • 支持不同实体根据状态决定当前是否需要释放/接管格子(例如漂浮单位可以“占用零个”格子);
  • 简化事务化占用逻辑的判断条件(只判断目标格子是否空即可,不必处理旧格子释放冲突)。

总结

我们现在的策略是:

  • 每个普通实体始终只占一个格子
  • occupying 明确记录当前占用位置;
  • 跳跃中不再占用“来源”格子;
  • came_from 辅助记录来源,供跳跃动画等使用,不影响占用状态;
  • 特殊情况(如大体积单位)可通过不同机制处理。

这样,整个系统更加清晰、高效,并避免了跳跃过程中的竞争条件和占用冲突。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行游戏,发现traversables的着色按预期工作

现在我们可以看到,蓝色的占用效果已经如预期般工作了。当角色移动穿越时,一旦角色的头部进入目标格子,系统立刻将该格子标记为被占用,然后跳跃完成。这表示实体在跳跃过程中对目标格子的接管是即时且准确的。


当前跳跃机制表现回顾:

  • 在角色“头部”进入目标格子的一刻,目标格子就会被设为 occupying
  • 原来格子的占用状态会被释放;
  • 整个过程保持了实体在任意时刻只占一个格子;
  • 视觉表现也能反映出这一机制,逻辑清晰可靠。

接下来打算实现的功能:

我们希望能在运行时动态添加多个英雄角色,便于测试和验证多人情况下的占用机制是否正确运行。
因此,我们计划引入一个快捷键,按下后可以立刻在场景中添加一个新英雄。


实现该功能的意图:

  1. 验证并发占用逻辑
    检查当多个角色在相近区域跳跃、移动时,是否能够正确处理格子占用冲突,避免重叠。

  2. 测试事务占用系统
    新增角色时,必须确保其初始位置不会占用已被他人占用的格子。可以观察事务性占用逻辑是否阻止了非法占用。

  3. 提升开发效率
    多角色切换与插入更利于调试跳跃过程、占用状态同步、视觉表现等复杂交互。


后续预期效果:

引入该功能后,我们将能够在多个实体之间进行跳跃测试,模拟真实的游戏场景,确保:

  • 不同角色之间不会“踩到”彼此的格子;
  • 跳跃过程中如有冲突,事务性占用逻辑能及时阻止跳跃;
  • 占用状态始终保持干净清晰,无残留占用状态。

这个功能将成为我们调试并验证整个跳跃系统稳定性的重要工具。
在这里插入图片描述

在这里插入图片描述

game_world_mode.cpp:重新实现添加新英雄的功能

当前目标是重新实现“添加玩家”的功能,使其在游戏运行过程中能够通过按键动态地添加一个新的玩家实体。


当前已有的机制分析:

目前已有一段添加玩家的逻辑,是通过 controller_start_add_player 实现的,作用是在游戏启动或初始化阶段添加一个玩家。但我们现在的需求,是在游戏运行中可以按键动态添加玩家,用于调试和验证系统功能。


新的实现思路:

我们打算将这段“添加玩家”的逻辑,移动到游戏主逻辑中,并由某个按键触发,这样我们可以随时添加新的玩家而不是只能在初始化时添加。

具体做法:

  1. 确定触发按键

    • 我们目前还没有决定使用哪个按键;
    • 想先临时用一个调试用的按键来实现,比如 Alt 键,稍后可以更换;
    • 需要检查该按键是否会和现有逻辑冲突。
  2. 修改按键输入逻辑

    • 在输入处理代码中,添加对所选按键的检测;
    • 当检测到按键按下,调用 add_player 逻辑;
    • 添加之前先检查是否已经添加过相同的玩家,防止重复添加。
  3. 防止重复添加

    • 如果 entity index 已经存在于玩家控制列表中,则不再添加;
    • 这一点需要在输入逻辑或添加逻辑中加以判断。

实施效果预期:

  • 游戏运行中按下某键,会即时创建并添加一个新玩家;
  • 多个玩家可以同时存在,用于测试碰撞、跳跃、占用等机制;
  • 添加行为受控,不会造成重复添加或状态混乱。

后续考虑:

  • 根据开发需要,将调试用按键替换为更合适的组合键或 UI 按钮;
  • 每个新玩家可自动分配不同的控制方式或颜色以区分;
  • 可以进一步扩展成一个“玩家管理面板”用于动态添加/移除/控制多个实体。

这个改动是整个调试过程中非常有帮助的一步,它会显著提升我们在多人交互、路径冲突等复杂场景下的测试效率。

运行游戏,尝试添加新英雄,享受怪异的效果

目前我们实现了目标功能:可以在游戏运行时通过按键动态地添加玩家角色,同时角色间还具有简单的物理交互,例如碰撞和弹跳。虽然弹跳现象有些奇怪,但它确实是因为之前引入的物理系统所产生的结果。


现状总结:

  • 新增玩家角色功能已完成;
  • 玩家角色被添加后可以与现有角色发生碰撞;
  • 碰撞后会出现弹跳效果,这是因为物理系统自动处理了接触响应;
  • 控制器仍需完善,尤其是在处理新玩家加入后的控制绑定问题。

存在问题与细节优化:

  1. 速度未归零
    新角色在添加时如果不重置速度,可能会出现意外移动或残留运动状态。
    需要在角色初始化或添加时将其速度向量设为零。

  2. 头部寻路逻辑过于自由
    当前系统允许角色的“头部”可以在未受限制的情况下移动至任意可达点,甚至跳过被占用的位置。
    需要改进跳跃/移动逻辑,优先判断目标点是否可占用,否则跳过或尝试其他位置;
    增加跳跃动作对占用信息的依赖,从而限制不合理的远距离移动行为。

  3. 启动流程被破坏
    新的玩家加入逻辑改变了系统对“已有玩家”的判断方式,影响了启动流程中模拟按键行为的部分逻辑。
    系统原本通过检测是否存在英雄角色来决定是否触发“开始游戏”模拟操作;
    修改后在游戏开始时没有检测到英雄角色,从而导致行为失效。


解决方向:

  • 在游戏初始化阶段,确保角色添加逻辑在“开始游戏”模拟操作之前完成;
  • 优化“是否已有玩家”判断机制,使其兼容动态添加方式;
  • 增强输入系统支持,比如针对每个玩家分配唯一控制器,防止控制冲突;
  • 针对头部与身体的移动分离逻辑,引入更精确的状态判断,比如“准备跳跃”“正在跳跃”“着陆中”等状态。

当前效果总结:

  • 功能上达成了预期目标;
  • 存在一些细节问题需要进一步梳理和优化;
  • 整体逻辑仍处在搭建阶段,需要对控制器管理、物理交互、占用状态管理等模块继续完善。

后续的工作应集中在提升系统的稳定性与可控性,特别是在处理多个实体并存时的行为一致性和输入管理层面。整体方向正确,已经迈出了关键一步。
在这里插入图片描述

game_world_mode.cpp:考虑让AddPlayer函数更健壮

当前我们正在处理游戏角色动态添加过程中出现的一些结构性问题,主要集中在模拟区域(Sim Region)与实体(Entity)系统之间的数据同步与更新机制。以下是对当前逻辑与存在问题的详细总结:


现有逻辑分析:

  1. 添加角色(Add Player)流程问题

    • 当前 add_player 函数在角色添加时,并未直接将实体加入到模拟区域中,而是只将其添加到了全局存储(World Storage)中;
    • 因为模拟已开始运行,模拟区域不会自动感知到全局存储中的新实体;
    • 导致某些逻辑(例如判断是否有角色存在)失败,因为这些逻辑依赖的是模拟区域中的数据而非全局存储。
  2. 英雄存在判断失败

    • 启动逻辑中通过检查模拟区域中的英雄实体来判断是否已有人物加入;
    • 新增角色未注册到模拟区域中,判断逻辑自然失败;
    • 虽然角色实体在系统中存在,但在模拟视角下“并不存在”。

问题的根本原因:

  • 实体被添加进了“错误的”地方——它进入了“世界存储”,却未被推入当前活跃的“模拟区域”;
  • 当前 add_player 逻辑绕过了模拟区域的标准流程,导致“添加”在逻辑上不完整;
  • 缺乏一个统一的、上下文感知的实体添加流程。

优化建议与设计方案:

  1. 建立统一的实体添加路径

    • 创建一个通用的 add_entity 接口,该接口根据当前是否处于模拟模式决定将实体:

      • 直接加入到模拟区域(Sim Region);
      • 或者推入世界存储区(World Storage);
    • 保证代码逻辑一致,避免手动分支导致的同步问题。

  2. 模拟区域即时注册机制

    • 当处于模拟状态时,应立即将新实体加入模拟内存块;
    • 可通过封装一个“即时解包并写入 SimSlot”的函数来实现。
  3. 考虑结构效率与一致性平衡

    • 虽然直接写入模拟区域实体槽效率更高,但需要手动维护结构完整性;
    • 而统一走 Pack → Unpack 流程可以保持所有代码路径一致、易维护,虽然性能略低;
    • 在当前阶段,优先推荐保持结构一致性,后续可根据性能需求优化为直接写入模式。
  4. 明确实体生命周期管理

    • 无论实体在哪添加,都应保证其“在模拟帧结束时被视为正式注册”;
    • 避免中间状态导致更新逻辑混乱。

总结:

  • 当前角色添加流程存在结构漏洞,需引入统一的实体注册机制;
  • 模拟区域与世界存储的数据需通过接口协调统一,不能分别处理;
  • 优化建议为:封装带上下文判断的 add_entity 接口,根据是否处于模拟状态决定添加路径;
  • 可以继续采用 Pack → Unpack 模式保证逻辑一致性,后续再做效率优化;
  • 修复此问题能大幅提升代码稳定性与行为一致性,是值得优先解决的基础问题。

该问题虽小,但若任其发展,将对后续多人交互与动态行为系统造成结构性隐患,因此应及时整理修正。

game_world_mode.cpp:在两种情况下都设置HeroesExist,运行游戏后决定保持AddPlayer原样

在这里插入图片描述

在这里插入图片描述

目前我们决定暂时采用现有的解决方式,其修复逻辑非常简单明确。我们判断是否已创建英雄,只需通过检查是否存在即可。这样实现起来直观清晰。

进一步思考后发现,目前采用的“打包再解包”(Pack → Unpack)流程其实也许正是最合理的方式。这种方式结构非常对称、统一,不需要为特殊情况单独编写路径逻辑,系统在结构上更加简洁稳定。

尤其是这个流程具备一些额外的优势:


保持结构统一性:

  • 无论实体是何时创建的,它们都走相同的流程:打包 → 写入世界 → 在下一帧开始时解包进入模拟;
  • 所有对象都在帧开始统一加载,没有中途插入的行为,减少状态混乱风险。

确保时间一致性:

  • 所有实体都会在下一帧开始时加入模拟区域;
  • 不会出现某些实体在一帧中途加入而其他实体已完成更新的“半状态”问题;
  • 这一时序上的纯粹性可以为未来的行为同步、回放重构等高级机制打下良好基础。

代码简洁且易维护:

  • 不需要增加对“当前是否处于模拟模式”的判断;
  • 添加逻辑保持通用和中立,降低了逻辑分歧和维护复杂度。

当前阶段决策:

  • 决定继续沿用现有的打包解包添加流程;
  • 不再尝试中途将实体直接加入模拟区域;
  • 采用“下一帧生效”策略,实现简单、逻辑清晰、结构统一、状态纯粹。

后续优化方向(可选):

  • 若将来对效率有更高要求,仍可考虑为少数高频添加场景优化直接插入流程;
  • 但需要确保不会破坏当前时间同步和结构对称性的优点。

综上,当前策略已满足正确性与可维护性的平衡点,因此决定保持现状,不再修改,暂时告一段落。

问答环节

这个调试世界每天都变得越来越怪异

确实,调试阶段中的世界状态常常变得非常混乱,这种混乱几乎可以说是调试代码的常态。在这个过程中,时常会产生一些“调试怪物”,各种看上去不正常甚至“恐怖”的状态和现象层出不穷。


调试过程中常见的问题:

  • 对象状态异常:某些数据未初始化或状态不一致,会导致角色位置、动作等出现异常;
  • 视觉畸变:尤其是在图形编程中,比如搞错了矩阵变换或顺序,哪怕只是个微小的变换 bug,结果就可能是角色被极度拉伸、压扁甚至完全“解构”;
  • 资源错位或重叠:在物理系统或碰撞系统中,如坐标计算错误、同步失败等,常会导致角色彼此重叠、互相弹飞、卡死等问题;
  • 逻辑错位:比如我们刚才讨论的实体是否正确进入模拟区域、何时被添加等问题,在调试状态下往往更容易暴露不一致性。

2D 程序相对温和:

相比 3D 来说,2D 调试的“画面灾难”要轻一些。因为我们只处理平面信息,就算出现错误,也只是表现为对象错位、压缩、拉伸等,在视觉上不至于那么“惊悚”。不像 3D 中,一个错误的骨骼变换或不对称矩阵可能直接导致角色身体被反折、肢体乱飞等。


当前场景下的问题:

我们在调试过程中发现:

  • 新增的克隆角色有些行为不符合预期;
  • 出现的某些视觉与行为异常也许只是由于调试状态下数据未初始化或状态残留;
  • 虽然能运行,但逻辑上不够健壮、容易出现边缘行为;
  • 这种混乱在实际开发中难以完全避免,需要通过明确划分“调试模式”和“正式模式”来应对。

结论与建议:

  • 调试代码常常会制造临时性的混乱结构,接受它是过程的一部分
  • 2D 视觉上的容错性比 3D 高一些,适合初期原型与调试验证
  • 应尽早为调试代码构建清晰的边界和隔离机制,避免其影响正式流程
  • 克隆角色的状态要严格初始化,避免未定义行为蔓延至主逻辑

调试世界的混沌是构建复杂系统的“副产物”,我们需要的不是避免混乱,而是有能力从混乱中快速提炼出清晰问题并修复它。

为什么克隆的玩家头部移动到点上时不跳过去?

关于角色头部移动到某个点时是否触发跳跃的问题,这是一个值得深入考虑的问题。当前代码是在角色头部的控制器绑定中处理这件事的,但其实这并不是必须的。完全可以选择在角色头部控制器绑定之外的地方处理这部分逻辑。

通过观察代码,可以看到角色头部的控制器绑定中确实用到了相关逻辑,并且其作用范围在一定时间和空间上是有限的,最终会停止执行。举例来说,可以把跳跃触发的逻辑从头部控制器中抽离出来,放到更上层或者独立的系统中处理,这样可能会使整体设计更加灵活和清晰。

总结来说:

  • 目前头部移动触发跳跃的代码绑定在头部控制器中;
  • 这种设计可以被替换或优化,比如将触发逻辑放到控制器绑定之外;
  • 这样做可以使代码结构更合理,也方便以后对跳跃机制进行扩展或调整;
  • 头部控制器对跳跃逻辑的作用是有限的,有时可能不需要完全依赖它来决定跳跃行为。

game_world_mode.cpp:使跳跃动作无论是否由控制器控制英雄都发生

代码里提到一个计时器“timer recenter”的原因可能是因为代码需要知道它的状态,不过可以调整设计,使得跳跃功能不依赖于“connected hero”(连接的角色)这一概念,这样会更简单。

具体思路是,给被控制的主角设定一个“dummy”(虚拟)控制器,比如叫 con_hero_,这个控制器其实什么都不做,就是一个空壳。然后,在代码中初始化时,将“connected hero”设为这个空的、非活动状态的控制器。

之后,程序会检查所有的控制器(比如四个控制器),看看有没有哪个是和当前主角连接的。如果找到了,就用那个控制器;如果没有找到,就用之前的那个“dummy”控制器。这样就保证了无论是否真的有连接的控制器,后续的代码都能正常运行,不会因为找不到“connected hero”而出错。

总结如下:

  • 目前跳跃功能依赖“connected hero”,增加了耦合和复杂度;
  • 通过创建一个空的、无效的虚拟控制器来替代“connected hero”的缺失,保证程序的健壮性;
  • 检查所有控制器,找到真实连接的那个,如果找不到,就用虚拟控制器代替;
  • 这样设计后,跳跃等功能就不必强依赖是否有连接的控制器,提升代码灵活性和容错能力。
    在这里插入图片描述

运行游戏,生成大量英雄

代码设计调整后,无论是否有控制器连接到英雄头部,相关逻辑都会运行。这样,正常情况下角色跳跃和移动功能不会受影响,即使没有人为控制,代码依然会执行。

当切换英雄时,即使控制器暂时没有连接,控制英雄头部的代码也会继续运行,保证逻辑的连续性和稳定性,不会因为没有连接的控制器而中断或出错。这种设计增强了系统的鲁棒性,避免了因为控制器连接状态变化导致的逻辑异常。

game_world_mode.cpp:保持这些英雄不断跳跃

英雄头部有一个弹簧机制,始终将头部拉近身体,因此英雄会一直停下来。如果想让英雄朝某个方向移动并跳跃,就需要在没有连接控制器的时候,为英雄施加一个推动力。

具体来说,当英雄没有连接控制器时,需要给英雄的加速度(比如ddP.X)赋一个非零值,这样英雄才会有方向上的运动,才能看到跳跃的动作。这个推动力的初始化和更新是必须要做的,否则英雄会因为弹簧机制一直被拉回身体附近而停止不动。
在这里插入图片描述

一直往外跑挡不住不知道什么原因

game_sim_region.cpp:引入traversable_search_flag,并为GetClosestTraversable增加Unoccupied标记

当我们调用 get_closest_traversable 来为实体找到最近可行走区域并将其放置时,我们可能需要加入一个“未被占用”的标志,以确保实体不会被放置到已被其他对象占用的位置。

为此,我们会在 sim_region 中调用 get_closest_traversable 时传入一个新的搜索标志,比如 TRAVERSABLE_SEARCH_UNOCCUPIED = 0x1,用来表示搜索过程中只考虑未被占用的位置。

接着在 get_closest_traversable 的实现中,就可以根据是否设置了这个标志,来决定是否跳过那些已经被占用的可行走点,从而避免实体被放在不合适的位置上。这个机制为实体初始化、复活、克隆等功能提供了更可靠的落点逻辑。

我们实现了一个机制,允许在寻找最近的可行走位置时排除已被占用的位置。这一过程通过引入新的搜索标志 TRAVERSABLE_SEARCH_UNOCCUPIED 来完成。

我们在调用 get_closest_traversable 时,传入了一个包含该标志的标志位集,用以控制搜索行为。在实际的搜索逻辑中,当我们对候选位置 P 进行检查时,若标志中包含了 TRAVERSABLE_SEARCH_UNOCCUPIED,我们就额外检查这个位置的 occupier 字段。如果该字段为非空,表示该位置已被其他实体占用,则跳过此位置。否则将其作为合法的候选位置。

这样一来,系统可以确保在将实体(例如玩家)放置到世界中的时候,不会把它放在已经有其他实体的位置上,避免重叠或逻辑错误。

此外,我们也不再需要强制将新玩家放置在摄像机当前的 P 点,而是通过上述逻辑选取一个最近且未被占用的合理落点,提升了放置逻辑的智能化和鲁棒性。

在调试过程中,也提到了一些代码格式相关的困扰,比如自动缩进错位问题,影响可读性,但这并不妨碍逻辑本身的推进。整体来看,这种基于标志的灵活搜索机制增强了系统处理实体放置的能力。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行游戏,生成一排英雄

有Bug

在这里插入图片描述

我们现在需要测试刚刚实现的代码。从理论上讲,我们现在应该可以把新玩家放在已有玩家的身上,也就是说可以精确地放置在一个未被占用的、最近的可通行点上。

我们实际尝试这么做的时候,确实发生了一些事情,玩家确实被放了下来,但视觉上看起来非常诡异。有点可怕的是,看上去像实现成功了,但又好像没完全成功。新玩家虽然确实找到了放置的位置,但他们并没有真正被放置在那个点上。这说明放置逻辑可能存在问题。

于是我们开始检查 add_player 的具体逻辑。发现这个函数并没有将玩家实体真正添加到世界上的某个实际位置上。虽然找到了落点,但代码没有把实体的实际坐标更新为那个位置。

由此我们确定:add_player 在位置处理上有缺陷。我们需要在添加玩家实体的时候,确保它被正确地放在我们已经计算好的那个落点位置上。当前的做法只是把它加入了世界实体列表,但没有给出它的确切空间坐标。

接下来,我们需要修复 add_player 的实现,使其不光找到可放置的位置,而且要真正把实体的初始位置设置为这个点,确保它正确地出现在世界中的那个位置上。只有这样,整个系统才会如预期般正常运作。

game_world_mode.cpp:让AddPlayer函数接收SimRegion以便调用GetClosestTraversable

我们现在的目标是实现一个功能:能够将玩家实体放置在我们当前周围的可通行位置上。为了实现这个目标,我们正在处理与模拟区域(sim region)和世界坐标相关的逻辑。

在这个过程中,我们意识到一个问题:调用 add_player 时,需要提供的是世界坐标下的位置,但我们当前找到的可通行点是模拟区域内部的位置。这就需要一个从模拟区域坐标到世界坐标的转换。

我们的思路是利用已有的模拟区域原点(sim_region_origin)来完成这种坐标映射。这个原点本质上定义了模拟区域在整个世界空间中的位置。所以我们可以用 map_into_chunk_space() 或类似逻辑将一个模拟区域内的相对坐标转换为世界坐标。

我们尝试的方案是:

  1. 获取模拟区域的原点。
  2. 调用 get_sim_space_traversable_standing_on() 这类方法获取玩家可以站立的点。
  3. 使用转换函数将这个相对点变为世界坐标。
  4. 最后将转换后的坐标作为参数传递给 add_player,实现正确的实体放置。

虽然整个逻辑有些复杂,但核心思路是清晰的。关键点在于:

  • 转换逻辑的正确性:模拟区域内的点需要加上 sim_region_origin 才能成为世界坐标。
  • add_player 要接受并使用世界坐标进行实体初始化
  • 模拟区域在添加玩家时是活跃的,所以我们始终有对应的 sim_region 可用。

最后,我们初步实现了这一逻辑,并测试了转换过程,虽然处理得较快,但整体方案是合理的,也确实能解决我们想要的问题:让玩家出现在正确的位置上。这个处理虽然临时完成得比较快,但效果看起来是可接受的。
在这里插入图片描述

在这里插入图片描述

运行游戏,发现问题仍未完全解决

我们当前实现的逻辑仍然存在问题,尽管我们找到了一个可通行的位置,并进行了坐标转换,但实际添加玩家实体时,它依然是被放置在摄像机位置(cameraP)而不是我们期望的目标位置。这是因为 add_player 调用时传入的位置参数仍是 cameraP,而非我们计算出的实体位置。

我们需要进一步修正调用流程,使其使用正确的世界坐标点。接下来,我们的思路如下:

  1. 不再使用 cameraP 作为添加实体的位置:这只是一个临时的位置标记,无法反映我们想要放置实体的具体点。
  2. 使用实体位置替代 cameraP:我们应该从目标实体或交互点(如跳跃点、可通行区域)中获取位置,并将其作为玩家生成点。
  3. 修改 add_player 的调用参数:将位置参数从 cameraP 改为实体或计算得到的坐标。
  4. 清理冗余代码:例如此前定义的 *con_hero = something 其实没必要,可以删掉,避免混淆逻辑。
  5. 必要时临时添加飞行变量:为了简化调试或过渡测试阶段,可以考虑加入一个“飞行状态”或某种控制标记,以便让新实体暂时具备特定行为或状态,便于定位问题。

总之,这里核心的问题是调用添加玩家实体的逻辑时,位置参数没有被正确替换。下一步我们会调整代码,让它基于目标实体或位置点来生成实体,而不是简单地以 cameraP 为中心放置。这个修改会使玩家生成逻辑更准确、合理。

game.h:为controlled_hero添加DebugSpawn以便测试添加功能

我们正在实现一个调试生成系统,目的是在特定条件下生成玩家实体。具体操作流程如下:

我们将一个标志位 debug_spawn 设置为 true,表示需要执行调试生成。我们把这段逻辑集成到处理受控英雄的区域,并在检测到 debug_spawn 被设置时,执行我们自定义的“生成逻辑”。

我们修改了玩家生成的位置,不再直接使用 cameraP,而是传入我们自己计算的坐标 vp,用于生成玩家。然而,如果忘记重置 debug_spawn,就会导致玩家不断生成,因此我们在生成完成后及时清除该标志,避免无限生成的问题。

目前主要的问题出现在“选择可通行区域”这一步。我们想确保新的实体不会生成在已经被占用的位置上,因此加入了一个 traversable_search_unoccupied 标志,要求 get_closest_traversable 在查找时排除那些已经被其他实体占据的位置。

在实现过程中,我们逻辑是:

if (!(flags & TRAVERSABLE_SEARCH_UNOCCUPIED) || P->occupier == 0) {
    // 可以选择这个点
}

这个条件的本意是:如果没有要求“非占用”,那么直接允许;如果有要求,就必须确保该点没有占用者。然而这个判断似乎没有生效,因为我们期望其排除有占用者的点,但实际上并没有成功。

我们做了一些调试,例如尝试移除标志以验证是否和标志有关,结果发现并非标志本身的问题,而是 P->occupier 判断逻辑似乎并未如预期那样工作。这说明也许我们拿到的是结构的拷贝,或者没有正确访问原始数据导致 occupier 状态不正确。

当前结论如下:

  1. debug_spawn 机制基本正常,可控制实体生成。
  2. 坐标传递和替换也基本正确,使用了我们计算的坐标 vp
  3. get_closest_traversableoccupier 判断逻辑似乎存在问题,可能是数据引用或结构处理导致判断失效。
  4. 为避免无限生成,我们在生成后清除了 debug_spawn 标志。
  5. 接下来需要继续检查占用判断处的逻辑,确认是否拿到的是正确的可通行点状态。

整体逻辑已逐步清晰,离正确完成仅差一步排查 occupier 的问题。

在这里插入图片描述

在这里插入图片描述

game_sim_region.cpp:让GetSimSpaceTraversable复制Occupier信息

我们意识到一个关键问题出在数据的传递方式上。在处理“寻找最近可通行点”时,我们之前是按值返回这个点(by value),而不是引用(by reference)。由于是值传递,这导致某些字段(尤其是 occupier)在返回后就丢失了原始的真实信息,因为它没有被正确地复制或者更新。

这个问题的根源是我们需要这个点的数据能够包含当前占用者的信息(occupier),用于判断该位置是否空闲。然而,当前结构中我们并没有在复制时将 occupier 字段一并复制过来,导致判断 occupier == 0 永远为真或者行为不一致,从而无法正确过滤掉已被占用的位置。

我们考虑过是否可以始终用引用的方式返回数据,但由于当前处理的是空间点(可能需要做空间转换或复制),我们确实无法直接写回原始位置,因此值传递在某些场景下是必须的。尽管如此,我们还是可以接受这种做法,只要确保在值传递时,将所有关键字段(包括 occupier)都一并复制进去即可。

因此我们决定接受当前这种按值返回的方式,但需要显式地复制 occupier 字段,确保后续的逻辑判断能够生效。这样一来,get_closest_traversable 返回的点就能正确反映是否被占用了,筛选逻辑也就能正常工作了。

总结关键调整点如下:

  1. 目前使用的是值传递返回空间点;
  2. 原实现中未复制 occupier 字段,导致逻辑判断失效;
  3. 解决方法是明确复制 occupier 到返回的结构体中;
  4. 尽管引用传递可能更高效,但在当前上下文中不可行;
  5. 只要确保关键字段完整复制,按值返回也是可以接受的。

这一步完成后,整体逻辑就可以正常根据占用情况判断放置位置了。
在这里插入图片描述

运行游戏,生成一些正确定位的英雄

好的,现在一切已经就绪,可以完成收尾工作。

我们已经正确实现了查找“最近未被占用的可通行点”的逻辑。通过确保在返回该点的数据结构时,将 occupier 字段也一并复制进来,系统现在可以准确判断某个点是否已经被实体占用。这个修复确保了我们不会将玩家放置到已有实体占据的位置上。

我们之前所做的准备工作如下:

  1. 添加了一个新的标志位 traversable_search_unoccupied,用于控制是否只寻找未被占用的可通行点。
  2. get_closest_traversable 中,根据是否设置该标志来判断是否排除已被占用的点。
  3. 修复了由于按值返回结构体而导致的 occupier 字段未正确复制的问题。
  4. 在添加玩家时不再简单地以 cameraP 为基准点放置,而是通过查找最近未被占用的可通行点,转换为世界坐标后,作为玩家的实际放置位置。
  5. 增加了一个调试变量 debug_spawn,用于控制是否在特定条件下触发玩家生成逻辑,并确保该变量在生成后被清除,防止持续生成。

至此,玩家生成系统变得更健壮、更智能,能够自动选择一个安全、未被占用的位置进行放置,提升了系统在复杂场景下的适应能力,也为后续的扩展留下了良好基础。下一步,我们可以考虑进一步测试不同边界情况,例如多个玩家同时生成,或者尝试生成在狭小空间中等,来验证这个系统的稳定性和鲁棒性。

game_world_mode.cpp:让DebugSpawn不覆盖已有设置

在 Handmade World 模式中,我们在进行调试生成(debug spawn)时,现在做出一个调整:不再让新生成的实体替代当前实体,而是直接从当前实体的位置生成一个新的角色。这意味着当前的实体保持不变,我们只是从它的位置“派生”出一个新的实体。

这种做法有几个优点:

  1. 不覆盖已有实体:原本的实体不再被替换,避免了意外丢失或状态中断的问题。
  2. 明确角色生成机制:清晰地区分了“生成新角色”和“接管现有角色”的逻辑路径,使系统行为更可预测。
  3. 更方便调试:我们可以多次从同一个位置生成多个实体,便于观察行为、验证碰撞处理、可通行点查找等逻辑。
  4. 保持原始控制状态:原实体的控制状态不变,新生成的实体默认不受控制,除非手动指定控制器。

通过这个改动,调试模式下的行为变得更符合直觉,同时也降低了调试过程中对现有游戏状态的干扰,便于开发和问题定位。我们接下来可以继续验证此方式下生成实体的完整性,例如是否正确落地、物理状态是否一致、是否避开已被占用的点等。
在这里插入图片描述

在这里插入图片描述

最后研究一下为什么没有碰撞

和树碰撞断点一直没进来

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述