下载 App

开发者日志01:用 GDScript 撑起上万个同屏角色

修改于10/2115 浏览
项目开工时定了三条约束,后面几乎所有技术决策都是从这里推出来的:
低配也要流畅。1920×1080 下平均 60 FPS,1% 低帧不低于 45 FPS。同屏有上千个近景角色、上万个远景实例,还有几百个动态对象。 工期短,只有一个程序。系统数量要少,耦合要低,改动要能快速验证。 美术只出白色剪影。明暗、起伏、晃动、饱和度全部交给着色器。 第一条直接否掉了"一个对象一个节点"的写法。几千个节点各自跑 _physics_process,光是 GDScript 的调用开销就超出预算。所以大批量对象一律是"结构数组存数据 + MultiMesh 画出来",节点数量和对象数量无关。
  1. 分层:逻辑层不认识场景树 scripts/ logic/ 纯逻辑:继承 RefCounted,只吃参数、吐结果 systems/ Node:持有逻辑模型,把数据画出来 ui/ 界面 autoload/ Config / Assets / Settings / Run config/ BalanceConfig 资源定义 logic/ 下的类有三条硬规矩:继承 RefCounted,不访问任何节点,不读全局单例(配置以参数传入)。这样它们可以在 headless 模式下直接 new() 出来测,结果也完全确定。
systems/ 下的节点只做两件事:在物理帧里把输入交给逻辑模型,在渲染帧里把模型的数组写进 GPU 缓冲。
固定的更新顺序 Godot 的节点处理顺序取决于场景树结构,改一下节点位置就可能改变行为。这里不依赖它:关卡控制器 LevelController 在自己的 _physics_process 里按固定顺序调用各个系统,其他系统节点不写 _physics_process。物理帧固定 60 Hz,所有逻辑都在物理帧里推进;_process 只负责表现(写缓冲、更新 uniform、相机、HUD)。
暂停用的是 SceneTree.paused 加分层的 process_mode:世界节点是 PAUSABLE,控制器、音乐和界面是 ALWAYS。不同的暂停场景共用这一套机制。
场景文件只有 4 行 scenes/ 下的 .tscn 只有一个挂着脚本的根节点,整棵树都在代码里搭。好处有三个:diff 清楚,不会出现 .tscn 合并冲突,每个节点为什么存在都能在代码里找到出处。
新关卡通过继承 LevelController 并覆盖几个钩子方法实现,是标准的模板方法模式:

搭建本关的地面与场景道具;返回远景实例不放置的地面矩形。

func _build_arena() -> Array[Rect2]: ...

本关的完成条件,每个物理帧在各系统之后调用。

func _tick_goal(dt: float) -> bool: ...

相机跟随的地面目标点。

func _camera_target() -> Vector2: ...

每个渲染帧最后调用,更新本关特有的表现。

func _render_arena(_delta: float, _real_dt: float) -> void: ... 3. 数据布局:结构数组 + 空闲栈 动态对象池是典型的 SoA(Structure of Arrays):每个属性一条 Packed 数组,按槽位下标对齐。容量在 setup() 里一次分配好,运行期间不再扩容。
var alive: PackedByteArray = PackedByteArray() var pos: PackedVector2Array = PackedVector2Array() var hp: PackedFloat32Array = PackedFloat32Array() var flash: PackedFloat32Array = PackedFloat32Array()

……

var _free: PackedInt32Array = PackedInt32Array()
func spawn(p: Vector2, hp_value: float, look: float = 0.5) -> int: if _free.is_empty(): return -1 var i: int = _free[_free.size() - 1] _free.resize(_free.size() - 1) alive[i] = 1 # …… 空闲槽位用一个栈管理,分配和释放都是 O(1)。初始化时按倒序压栈,前几次分配拿到的是低位下标,活跃对象自然集中在数组前部。相比一个对象一个 RefCounted,这样没有对象分配,没有引用计数,也不需要逐个释放。
  1. 空间哈希:每帧用计数排序重建 所有邻近查询都走自己写的均匀网格,没有用物理引擎。需求只是"圆和圆的距离判断",物理引擎在这里太重,而且结果不好单测。
网格用的是类似 CSR(压缩稀疏行)的布局:cell_start 长度为单元数 + 1,记录每个单元在 cell_items 里的起点;cell_items 是按单元排好序的点下标。重建是两遍扫描加一次前缀和,复杂度 O(N):

节选自 SpatialGrid.rebuild()

第一遍:统计每个单元的点数

for i: int in n: var p: Vector2 = points[i] var c: int = cell_y(p.y) * cols + cell_x(p.x) _cell_of[i] = c _counts[c] += 1

前缀和:每个单元在 cell_items 里的起点

var running: int = 0 for c: int in cols * rows: cell_start[c] = running running += _counts[c] cell_start[cols * rows] = running

第二遍:按单元把点下标写进去

_counts.fill(0) for i: int in n: var c: int = _cell_of[i] cell_items[cell_start[c] + _counts[c]] = i _counts[c] += 1 查询结果写进一个复用的成员数组 result,只有前 result_count 个有效,查询过程不分配内存。区域外的点归入最近的边缘单元,不会越界。
它的单元测试很直接:随机单元尺寸(0.3 到 3 米)、随机点集(故意放一部分在区域外)、随机查询圆,20 轮 × 30 次查询,结果必须和暴力遍历逐个相同。
  1. 人群模拟:只算醒着的人 近景人群是上千个代理的分离 + 回流模拟。如果每帧对所有人做邻域分离,开销和总人数成正比。实际上绝大多数人站在原位一动不动,所以引入了休眠:
偏离原位不到 1 毫米、速度接近 0、又不在焦点附近的代理不参与计算。 醒着的人挤到睡着的人时,把对方标记为 2(本帧刚醒),加入活动列表,下一帧正常参与计算。标记值和 1 区分开,避免同一对被处理两次。 两个醒着的人只由编号小的一方处理这一对。 这样每帧的计算量只和"被扰动的人数"有关,总人数可以放得很大。
回到原位用的是临界阻尼弹簧。配置里写的是直观的"从 2 米外回到 0.1 米以内的用时 T",运行时反算角频率。临界阻尼从静止出发时,偏移比为 (1 + ωt)·e^(−ωt),令它等于 0.05,解得 ωT ≈ 4.7439:

临界阻尼弹簧满足 (1 + x)·e^(−x) = 0.05 时的 x,用于由"回流目标用时"反算角频率。

const RETURN_X: float = 4.7439

setup()

omega = RETURN_X / cfg.return_time

_return_home():半隐式欧拉

var v: Vector2 = vel[i] + (-w2 * offset - damping * vel[i]) * dt vel[i] = v pos[i] += v * dt 两个小细节:
两点完全重合时没有分离方向。这里按编号以黄金角取一个固定方向(Vector2.from_angle(index * 2.39996323)),既避免除零,相邻编号的方向也天然错开。 分离阶段复用上一帧末尾重建的网格。每帧位移只有几厘米,候选单元基本不变,省掉一次重建。 开发机实测,450 个代理全部处于活动状态时,人群模拟每帧平均 0.6 ms,1% 最差 1.9 ms,在设计预算(2 ms)以内。
猜你想搜
聚光灯游戏创作挑战 gdscript性能优化
迟来的开发者日志02《人潮翻涌》---截图
迟来的开发者日志02《人潮翻涌》---
《人潮翻涌》开发日志:让每一站都有自己的风景 最近,《人潮翻涌》的制作重点放在了场景细节和整体视觉氛围上。我们重新走了一遍游戏中的七张地图,从拥挤的车站,到有河流穿过的景区,再到黄昏集市和海边,检查这些地方在实际游戏镜头下的样子。 场景的基本轮廓已经搭了起来,但有些角落还比较空,有些物件虽然摆进去了,却没有形成自然的联系。因此,这一轮主要围绕已有素材继续整理:调整摆放、补充小景,让不同地点拥有更明
5 赞
【童谣镇】聚光灯第二次开发日志截图
【童谣镇】聚光灯第二次开发日志
开发进度: 美术 1、主角人物立绘已细化完成 2、所有NPC的QQ人形象与呼吸动画已设计完成 3、大部分UI界面已设计完成 4、面包房建模与树屋建模以及基础房屋、路灯、树木模型已完成。 5、制作过场动效,制作通用动效材质ing 6、广场资产替换、永续桥和植被的资产替换、房屋资产替换,河流制作 7、学校模型的材质完成 8、石墩子、小道具模型制作ing 9、NPC正比胸像绘制ing 策划 1、任务系统
16 赞
10 回复
00:34
《荒原复苏指南》开发日志④:关卡演示堂堂袭来!!截图
《荒原复苏指南》开发日志④:关卡演示堂堂袭来!!
- 人类曾因掌握元素合成技术而一度辉煌,又因滥用元素能量而逐渐从内里衰败。 - 30世纪80年代。社会文化充斥着激进乐观主义与庞大机械美学,在这里,污染和能源共生,幸福与痛苦并存。官方广播和宣传画里依旧在宣扬“科学将战胜一切”、“元素能量将再造新农田”的口号,而世界主城却已经因为污染过度而濒临崩溃、空间挤压严重。为了寻找新的生存空间与无污染能源,官方启动了“重构拓荒计划”。 - 玩家将作为前线拓展
18 赞
6 回复
00:07
雨天決行 开发者日志02-人工智能的自我修养截图
雨天決行 开发者日志02-人工智能的自我修养
又和大家见面啦!第二篇日志,说实话一开始不知道该怎么编排,但团队里惊喜的成果越来越多,我们已经迫不及待把更新的内容端上来给大家啦!那么事不宜迟,快来看看都有什么新情报吧ദ്ദി˶ー̀֊ー́ )✧ 一.美术(重磅升级) 以主美小为的生命力作为燃料(bushi)本次美术组取得了大进程!首先是各个ui界面的优化和细化,上次大家看到时还只是初具雏形的草稿,现在大多都完整起来了哦! 同时,随着配色的补全,手
43 赞
39 回复
【涌现符】开发日志#1截图
【涌现符】开发日志#1
大家好,我们是电子织机队,核心成员都来自同一个学校社团。 经过了五天的设计大烧烤终于把玩法大概定了下来。虽然题目一出大家立刻都想到了涌现式玩法,但是在这么短的时间内设计出优秀好玩的涌现式玩法对于没有太多经验的学生还是难度颇高,涌现式叙事对于没有专门编剧的我们队也难以达到理想的效果。 皮尔斯提出的"Symbols grow"是一个很有趣的概念,也即一个符号在使用中产生解释,解释引导预期和行动,经验又
4 赞
2 回复
开发者日志03截图
开发者日志03
大家好,第三篇开发日志来了。先公布一件事:我们的游戏正式定名为《尸涌》!这是一款以丧尸战斗和村庄探索为核心的第一人称射击游戏。这次也把最近几轮的战斗、成长、界面和探索进展一起整理出来。 故事从一位旅行者的失踪开始。主角作为她的好友,沿着她的旅行轨迹来到这个村庄,寻找她留下的线索,却逐渐发现村民与城里的制药公司之间,藏着秘密人体实验。村庄里的丧尸灾变究竟是怎么发生的?失踪的好友又去了哪里?这是我们刚
1 赞
开发日志01截图
开发日志01
“涌现” 1.读作 ‌yǒng xiàn‌,意思是(人或事物)在同一时期大量出现,或者突然出现。‌‌ 2.科学含义‌:在复杂性科学里,涌现指大量简单个体相互作用后,整体产生了单个个体不具备的新特性,也就是“整体大于部分之和”,比如蚁群表现出的智慧、人工智能大模型的推理能力。 刚看见这个题目的时候,有一点点懵。看了词语解释之后,才有了一点想法。 考虑到时间还有个人开发的能力限制,目前决定做一个纯粹的
2 赞
00:30
《失心》游戏开发者日志01|游戏UI迭代及实机演示截图
《失心》游戏开发者日志01|游戏UI迭代及实机演示
进入《失心》之前,玩家首先看到的是我们工作室 UNLOOK 的标识。我们将原来的标识转成 SVG,并让其中的红色瞳点转动、观察,再逐步显露完整字形,希望这个短暂的开场能带出一点好奇和不安。 这次《失心》的开局 UI,我们围绕小灵通(因为游戏的主要交互道具就是小灵通)做了几轮调整,希望玩家从打开游戏开始,就能感受到故事里的年代和气氛(故事年代背景为2000s)。 早期版本先确定了一个方向:把菜单放进
15 赞
10 回复
《涌》开发日志 #1:在从深圳去海南的船上,我想了一整天的玩法截图
《涌》开发日志 #1:在从深圳去海南的船上,我想了一整天的玩法
本期进展 这次聚光灯本来不打算参加的。大四了,事情一堆。后来看到赛制里可以做纯 H5——不用下载不用安装,浏览器点开就能玩——我转念一想:写代码的活儿反正交给 AI,我负责想玩法和挑毛病,那就参加吧。 开题那天是 10 月 1 号,我正好在从深圳回海南的路上。白天在车上晃,晚上还要坐过海的船,船上睡不踏实,就在那种又困又清醒的状态里,把玩法想了一整天。 想出来的就是《涌》。 一句话说:你在一卷展开
4 赞
1 回复
【开发者日志 #01】从玩游戏到做游戏:我们用记忆诠释「涌现」截图
【开发者日志 #01】从玩游戏到做游戏:我们用记忆诠释「涌现」
大家好呀,我们是【肝完就是胜利】组合! 最开始我们只是刷刷作品、玩玩游戏。万万没想到,现在居然能和一群三观契合、热爱创作的小伙伴一起,从“玩游戏的人”,变成了“做游戏的人”。在TapTap打磨属于我们自己的作品,真的是一件奇妙又幸运的事。 Gamejam【涌现】主题公布后,我们几个人陷入沉思。 好抽象!好哲学!我们反复挠头:玩家能看到什么?感受到什么?又能在游戏里收获什么全新的体验?让我们一时无从
5 赞
4 回复