《Defold游戏开发入门》1.3 Defold 核心概念

系统掌握集合、游戏对象、组件、精灵、图块、动画和物理碰撞的关系,建立可扩展的 Defold 场景心智模型。

1.3 Defold 核心概念

Defold 的学习分水岭,不在于你是否记住了某个 API,而在于能否看懂一个运行中场景由什么组成。只要把场景视为一堆图片或一段大脚本,项目一变大就会失去控制;一旦理解集合、游戏对象和组件之间的职责,问题就能被拆成可处理的单位。本节建立这一心智模型,并用一个平台跳跃场景贯穿说明。

三层积木:集合、游戏对象、组件

集合是一个场景或场景片段的容器。主菜单、关卡、战斗场景、HUD 都可以由集合组织;集合还可以嵌套别的集合,从而把重复出现的内容封装起来。集合负责“这里有哪些对象、它们初始在哪里”,而不应该承担所有玩法规则。一个项目可以有多个集合,但启动时会从 game.project 指定的 bootstrap collection 开始。把启动层保持轻薄,负责装载菜单、主控制器和当前场景,会让后续切换更清楚。

游戏对象是世界中的实体单位。它有位置、旋转、缩放和 id,但它本身并不会自动显示、碰撞或思考。玩家、敌人、金币、镜头锚点、不可见的关卡控制器都可以是游戏对象。这个设计很重要:不可见不代表不需要对象。把计时器、刷怪器、音乐控制器做成独立对象,通常比塞进玩家脚本更容易理解和复用。

组件才是对象获得能力的方式。sprite 组件显示一张图或图集动画;script 组件运行 Lua;collision object 组件参与物理;sound 组件播放音频;label 或 GUI 则服务于文字和界面。一个对象可以有多个组件,组件也可以通过 id 被单独寻址,例如 #sprite#collisionobject。因此,“玩家是什么”并不是一个固定类,而是一个具有合适组件组合的对象。这种组合思维能让你轻松创建没有物理的装饰物、没有精灵的触发器,或带多个视觉组件的 Boss。

用平台跳跃场景理解层次

假设要制作一个最小平台跳跃关卡。可以建立 level_01.collection,其中包含 playerlevel_geometrycoin_spawnercameragame_controllerplayer 有 sprite、collision object 和 script;level_geometry 有 tilemap 和静态碰撞体;coin_spawner 只有脚本和工厂引用;game_controller 只有脚本,负责状态和关卡结束。这样的层次已经透露出一个重要原则:实体行为由实体本身管理,跨实体规则由协调者管理。

如果玩家踩到金币,金币的脚本应负责“我已被收集、播放动画、通知外部”;玩家脚本可负责移动和受伤;控制器负责把收集事件换算成分数并通知 HUD。不要让金币直接修改 GUI 的文字,也不要让 GUI 反过来删除金币。这不是形式主义,而是在预防后期的连锁依赖。以后增加双倍分、成就或联网同步时,明确的事件边界会节省大量重写工作。

原型、实例与生命周期

编辑器中放进集合的对象通常是实例。它可以引用一个 game object 原型,也可以在集合内直接定义。原型适合反复出现且结构稳定的实体,例如不同关卡中的玩家、普通敌人、可破坏箱子。实例则保存位置、局部属性或少量覆盖。这个区分的目标是减少复制:如果十个敌人的碰撞体尺寸都变了,你希望改一次原型,而不是逐个打开十个关卡。

对象的生命周期同样要被主动设计。对象在集合加载时创建,也可以通过工厂动态生成;它可以被禁用、删除,或随集合卸载而消失。动态创建适合子弹、粒子、掉落物和程序化敌人,但要同时思考谁拥有它、何时回收、是否需要对象池。创建不是免费的,短时间内大量生成和删除对象可能造成帧峰值。先让行为正确,再根据 profiler 的证据决定是否引入对象池,而不要一开始就为想象中的性能问题写复杂系统。

坐标、变换与父子关系

每个游戏对象都有局部变换:位置、旋转和缩放。若对象成为另一个对象的子对象,它的局部坐标会相对父对象计算。角色手里的武器、随角色移动的阴影、绑定在头顶的血条都适合这种关系。父对象移动时子对象自然跟随;但这并不表示所有关联都应使用父子层级。游戏逻辑上的“敌人属于某关”通常只需要数据或消息关系,强行建立深层父子树会使坐标和销毁顺序更难预测。

世界坐标和屏幕坐标也必须区分。玩家、敌人和关卡通常在世界中移动;HUD、暂停菜单和按钮通常固定在屏幕上。镜头改变的是世界被看见的方式,不应让 HUD 跟着滚动。很多“UI 跑出了屏幕”的问题,本质上是把界面做成了世界对象,或把世界对象写进了 GUI 坐标体系。为每一项内容先确定它属于世界还是屏幕,可以避免大量返工。

精灵、图集与动画

sprite 组件是最常见的视觉组件。它通常引用图集(atlas)中的某个图像或动画。图集把多个小图打包在一起,减少纹理切换,也让角色的待机、跑步、跳跃帧序列有一个统一来源。导入图片前先确认尺寸、透明通道和像素风是否需要关闭平滑;图像质量问题常常来自源文件和采样设置,而不是代码。

动画的核心不是“帧越多越好”,而是状态和事件。角色处于 idle、run、jump、hurt 哪种状态时应播放哪段动画;跳跃从上升转为下降时是否切换;攻击命中帧是否要触发判定,这些都应由规则明确决定。可以用 sprite.play_flipbook() 切换动画,但应避免每一帧重复调用同一动画。先保存当前动画状态,只有发生变化时才切换,既避免无意义调用,也让日志更清晰。

图块地图(tilemap)适合铺设大面积、重复度高的关卡背景和地形。把草地、砖块、斜坡等切成网格图块后,设计者可以快速绘制关卡,而不必摆放数百个独立精灵。视觉图块和物理图块不必完全相同:看起来是灌木的图块未必能阻挡角色,隐形边界也可能没有对应的美术。把“可见”与“可碰撞”分别设计,是关卡可维护性的基础。

物理与碰撞:先选对对象类型

碰撞对象赋予游戏对象物理行为。静态对象适合不会移动的地面和墙壁,动态对象由物理引擎模拟,适合需要质量、摩擦、弹性和力的箱子或球。还有用于检测而非实体阻挡的触发器思路。开始实现前先问:它要被物理引擎推着走,还是只要知道两个区域相交?前者适合动态物体,后者通常适合触发检测;用错误类型硬凑出效果,会让控制手感和性能都变差。

碰撞形状也要尽量简单。矩形、圆形等基础形状计算便宜且稳定;复杂轮廓只在确有必要时使用。角色的美术边缘不必与碰撞形状完全贴合,略宽容的形状往往能带来更好的手感。平台跳跃角色通常需要一个略窄的身体形状,避免脚尖擦墙即被卡住;拾取物的触发范围可以比图像略大,让玩家不会觉得“明明碰到了却没拿到”。这是体验设计,不是作弊。

物理消息是对象通信的一种来源。碰撞发生时,相关脚本会收到消息;代码应先检查对方的 group 或约定的标记,再决定行为。不要仅凭对象 id 字符串判断“这是敌人”,因为同类对象会有很多实例。建立清晰的碰撞分组和 mask 规则,例如 player 与 enemy、world、pickup 相交,而 bullet 不与 pickup 相交,能显著减少无用检测和误触发。

场景管理与通信边界

集合不是传统意义上永远常驻的“关卡文件”。它可以被加载、代理、卸载或作为子集合嵌入。简单项目可以直接在一个主集合中保留当前关卡;较大的项目则应把菜单、游戏、结算拆成独立集合,通过 collection proxy 或加载控制器切换。无论采用何种方式,都应明确切换契约:旧场景何时停止接收输入,哪些全局服务保留,加载失败时显示什么,进入新场景后由谁开始一局。

跨对象通信优先使用消息。消息是异步的,发送方只描述“发生了什么”和必要数据,而不需要知道接收方内部如何实现。例如 msg.post("main:/game_controller#script", "coin_collected", { value = 10 }) 表达的是事件,不是命令 GUI 把某个标签改成某个字符串。接收方在 on_message() 中验证消息 id 和数据。消息地址的 collection、game object、component 三段含义要分清;地址写错时先打印发送者和接收者,而不要猜测引擎是否漏发。

常见架构错误与修正

第一个错误是“万能控制器”。它读取所有输入、移动玩家、生成敌人、计算分数、更新界面、播放音乐。起步时很快,扩展时却极难。修正方法不是过度抽象,而是每出现一个稳定职责就拆出一个对象:玩家控制、生成器、HUD、音频控制器、关卡规则各有边界。

第二个错误是“所有东西都能直接访问”。如果每个脚本都持有其他对象的地址并直接调用细节,修改一个对象会波及一大片。修正是以消息传递事件,以少量配置暴露必要参数,并把共享规则放进独立模块。第三个错误是“用画面代表规则”。例如根据精灵是否可见判断金币是否被收集,根据动画名判断角色是否死亡。应保存明确的状态值,让画面成为状态的结果。

本节小结与练习

现在请独立搭建一个练习集合:一个有精灵和脚本的玩家、两块静态地面、一个触发区域和一个不可见控制器。要求玩家能左右移动,进入触发区时控制器收到消息并打印一次,HUD 不随镜头滚动。不要急于加入正式美术;用颜色不同的占位图也可以。完成后画一张对象关系图,标出每个对象拥有的组件、发送的消息和保存的状态。

如果你能解释“为什么控制器不应直接改玩家位置”“为什么金币与分数 UI 不应直接耦合”“为什么图块视觉和碰撞可以分离”,就已经掌握了 Defold 最重要的基础。后续所有 Lua、资源、物理和性能章节都只是把这些积木组合得更精确。下一章将从 Lua 本身开始,使这些组件真正拥有行为。

把概念落实为一次审查

开始一个新功能时,先做一次“对象审查”。列出要新增的对象,并分别写下它在世界中的身份、持有的组件、初始化数据、会发送的消息、会接收的消息及销毁条件。例如宝箱不是“一个需要打开的图片”,而是一个带精灵、触发区域和脚本的对象;它在被玩家触发后发出 chest_opened,停止参与碰撞,播放一次性动画,随后在动画结束或计时到达后删除。分数、奖励内容和存档更新则由控制器处理。只需几行笔记,就能提前发现“到底谁负责什么”的空白。

也要审查层级是否被滥用。父子关系用于空间变换和随动很自然:玩家移动时武器、阴影和头顶标记应该跟随。但若把 HUD、生成器、音频控制器都挂在玩家下面,它们会随着角色删除、缩放或转场而产生意外行为。逻辑依赖应使用消息、数据或明确的拥有关系表达;空间依赖才使用父子变换。这个区分会让集合树在项目后期仍能被人读懂。

最后,为每一种可复用对象建立“最小独立测试”:在空集合中放入一个实例,确认它能初始化、接收关键消息、完成一次生命周期、被删除后不留下错误。对象能在空集合运行,通常也更容易在不同关卡复用。概念一旦落到这种小测试上,就不再只是术语,而成为日常开发的检查工具。

当对象开始动态生成时,再增加一个所有权检查:谁创建它,谁在场景切换时负责处理它,谁接收它的完成或失败事件。明确这些答案,能避免屏幕外对象、旧场景消息和资源引用残留等问题。

在代码评审时,可以沿着一次事件画线:输入从哪里进入,哪个组件保存状态,谁发送消息,谁改变规则,谁更新表现,何时销毁对象。若这条线跨越很多隐藏引用,说明对象边界需要进一步简化。这个方法适用于任何规模的 Defold 场景。

在此基础上再做一次命名检查:对象 id 表示身份,组件 id 表示能力,消息 id 表示事实。三者各自清晰,搜索、日志和跨场景复用都会更可靠。

集合文件也应只承担组合责任;一旦发现它被用来隐藏大量特例,就把特例迁移为配置或独立对象。保持集合可读,场景切换和版本比较才不会失去依据。

这种分层还能让美术、关卡和程序分别在适当资源上工作,减少相互覆盖同一文件的协作冲突。

边界越清楚,越能安全地迭代。

这也是项目从原型走向正式制作时最值得坚持的基础。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「defold」更多文章