1. 项目概述与核心思路
最近几年,独立游戏开发的门槛肉眼可见地降低了,这很大程度上要归功于像 Godot 这样强大又免费的开源引擎。我自己从 Unity 转过来用 Godot 也有一段时间了,最大的感受就是它的轻量、高效,以及对 2D 游戏开发近乎“原生”般的友好。这次,我想和大家分享一个完整的实战项目:用 Godot 4 从零开始打造一款 2D 像素风冒险游戏,我把它叫做《丛林探险》。这个项目非常适合想入门 Godot 或者想系统学习 2D 游戏开发流程的朋友。游戏的核心玩法很简单:玩家操控一只灵活的小狐狸,在一个充满未知与危险的丛林世界里探索、跳跃、战斗,目标是收集散落的宝藏并抵达终点。听起来像是经典的平台跳跃游戏?没错,但我们会在这个过程中,把 Godot 里从场景管理、物理系统、动画状态机到敌人 AI、UI 交互等核心模块都过一遍,让你不仅做出一个游戏,更能理解 Godot 的工作流和设计哲学。无论你是编程新手,还是有一定基础想换个引擎试试,跟着这个流程走下来,你都能收获一个可运行、可扩展的游戏原型,以及一套扎实的 Godot 开发方法论。
2. 开发环境搭建与项目初始化
2.1 Godot 引擎版本选择与安装
工欲善其事,必先利其器。第一步是安装 Godot。目前稳定版是 Godot 4.2 系列,我强烈建议直接从官网下载标准版(Standard version)。它包含了 C# 支持,即使你暂时不用 C#,留着也没坏处。避免使用 Mono 版本除非你确定要使用 .NET 进行开发,因为标准版对 GDScript 的支持最稳定,也是社区资源最丰富的版本。
安装过程极其简单,Godot 是绿色便携的,下载下来就是一个可执行文件,你可以把它放在任何你喜欢的位置,甚至放在 U 盘里随身携带。第一次启动时,它会让你选择一个项目存储路径,这不是安装路径,只是引擎存放用户配置和模板的地方,按默认设置即可。
注意:虽然 Godot 对系统要求不高,但为了获得流畅的编辑体验,尤其是处理像素艺术和粒子效果时,建议确保你的电脑有独立显卡,并更新到最新的驱动程序。我在集成显卡的笔记本上也能用,但在编辑复杂场景时,会明显感觉到视图port的帧率下降。
2.2 创建新项目与基础设置
启动 Godot 后,点击右上角的“新建项目”。给项目起个名字,比如“Jungle_Adventure”。关键的一步是选择渲染器。Godot 4 提供了前向渲染器(Forward+)和移动端渲染器(Mobile)。对于我们的 2D 像素游戏,选择“移动端”渲染器是更优的选择。它的设计目标就是高效、低开销,对于 2D 游戏和风格化 3D 来说性能表现非常好,而且能减少一些高级 3D 特性带来的潜在兼容性问题。项目路径选择一个空的文件夹,然后点击“创建并编辑”。
进入编辑器后,我们先进行几项重要的初始设置。打开“项目 -> 项目设置”。在“常规 -> 显示 -> 窗口”下,设置我们游戏窗口的初始大小。由于是像素风,我们不需要很大的分辨率,设置宽度为 384,高度为 216。这是一个非常经典的“伪复古”分辨率,在 1080p 显示器上可以整数倍放大(5倍)到 1920x1080,像素清晰锐利。同时,勾选“拉伸 -> 模式”为“canvas_items”,缩放模式为“viewport”。这个组合能确保我们的游戏画面以整数倍缩放,避免像素模糊,是像素游戏的标准配置。
另一个重要设置是物理。在“项目设置 -> 物理 -> 2D”中,默认的重力是 980 像素/秒²,方向向下。这个值对于平台跳跃游戏来说通常偏大,会导致角色下坠过快,手感很“沉”。我们可以先保持默认,后续在角色的脚本中根据手感再精细调整重力缩放系数,这样更灵活。
3. 游戏核心资源管理与导入
3.1 像素艺术素材的选择与准备
对于《丛林探险》这样的项目,美术资源是灵魂。如果你不是美术出身,完全不用担心,现在有大量优秀的免费或付费的像素艺术素材包可供使用。我推荐几个地方:itch.io上的 Game Assets 板块、OpenGameArt.org,以及一些知名的像素艺术家的个人商店。寻找素材时,要特别注意风格统一和素材完整性。你需要一套包含以下元素的素材:
- 角色精灵图(Spritesheet):包含小狐狸的 idle(待机)、run(奔跑)、jump(跳跃)、attack(攻击)等动画帧。最好是网格状排列整齐的图集。
- 瓦片集(Tileset):用于搭建关卡地形的各种地块,如泥土、草地、石块、树干、平台等。一个优秀的瓦片集应该包含“自动瓦片”(Autotile)功能,能让地形拼接自动化。
- 敌人与物品:比如史莱姆的精灵图、宝藏、金币、生命值道具等。
- UI 元素:血条、分数显示、按钮图标等。
- 背景与前景:多层滚动的背景图,营造丛林景深。
找到素材后,我强烈建议你用专业的像素画工具如Aseprite或免费的LibreSprite打开检查一下。确保所有素材的调色板相对一致,并且像素尺寸是统一的(比如都是 16x16 或 32x32)。不一致的像素尺寸会导致在 Godot 中缩放和碰撞形状对位困难。
3.2 Godot 中的资源导入与配置
将下载好的素材文件夹(建议命名为“art”或“assets”)复制到你的 Godot 项目根目录下。Godot 的文件系统面板会自动刷新并显示这些资源。
对于像素艺术,最重要的导入设置是纹理过滤模式。右键点击任意一张图片资源,选择“快速加载”或直接在导入面板中查看。在“导入”选项卡下,找到“纹理”设置。必须将“过滤”模式从默认的“Linear”改为“Nearest”。线性过滤(Linear)会进行像素插值,导致像素边缘模糊,彻底破坏像素风的锐利感。最近邻过滤(Nearest)则直接显示原始像素,是像素游戏的标配。你可以批量选择所有图片资源,然后统一应用这个设置。
对于角色和敌人的精灵图(图集),我们通常不直接使用,而是先创建一个SpriteFrames资源来管理动画。但更常见的做法是直接使用AnimatedSprite2D节点,并在其属性中创建动画帧。Godot 4 对精灵图集的切片(Slice)功能支持很好,你可以利用“SpriteFrames”编辑器中的“从精灵表添加帧”功能,自动按网格切割图集。
瓦片集资源(.tres 文件)的创建则通过 TileSet 资源完成。我们将在地图编辑部分详细展开。
4. 玩家角色:小狐狸的实现
4.1 角色场景结构与节点组成
在 Godot 中,一切皆场景(Scene),角色也不例外。我们创建一个新的场景,作为玩家的“蓝图”。根节点我通常选择CharacterBody2D。在 Godot 4 中,CharacterBody2D 是专门为需要自定义移动逻辑(比如受玩家输入控制)的物体设计的,它内置了与物理世界的碰撞检测和响应,比 RigidBody2D 更可控。
在这个 CharacterBody2D 下,我们添加以下子节点:
- Sprite2D:用于显示小狐狸的贴图。稍后我们会把它替换成 AnimatedSprite2D。
- CollisionShape2D:定义角色的物理碰撞形状。对于一个平台游戏角色,一个胶囊状(CapsuleShape2D)或矩形(RectangleShape2D)的碰撞体通常就够了。形状要略小于精灵的视觉轮廓,这样手感会更友好。
- Camera2D:玩家的跟随相机。勾选“正在播放”属性,并设置合适的拖拽和限位参数,让镜头移动更平滑。
- AnimationPlayer:用于控制复杂的动画过渡和信号触发。虽然 AnimatedSprite2D 可以播放动画,但 AnimationPlayer 能更好地管理不同状态(如落地、受伤)时的动画切换和逻辑。
- Timer节点(可选):用于实现如攻击冷却、无敌时间等功能。
将这个场景保存为“player.tscn”。
4.2 移动、跳跃与物理编程
接下来是核心脚本。为 CharacterBody2D 根节点附加一个新脚本,命名为“player.gd”。我们将使用 GDScript 编写逻辑。
首先定义一些导出变量,方便在编辑器中实时调整手感:
extends CharacterBody2D @export var speed: float = 130.0 @export var jump_velocity: float = -300.0 @export var double_jump_velocity: float = -250.0 # 从项目设置中获取重力值,并乘以一个缩放系数以获得更合适的手感 var gravity = ProjectSettings.get_setting("physics/2d/default_gravity") * 1.5 var has_double_jumped = false var is_attacking = false在_physics_process(delta)函数中,我们按帧更新角色状态。逻辑顺序通常是:处理输入、应用重力、执行移动、处理碰撞。
func _physics_process(delta): # 1. 应用重力(如果不在空中,则重置双跳能力) if not is_on_floor(): velocity.y += gravity * delta # 在空中时,可以检测是否允许双跳 else: has_double_jumped = false # 落地重置双跳 # 2. 处理跳跃输入(必须在应用重力后,移动前) if Input.is_action_just_pressed("jump"): if is_on_floor(): # 在地面上,执行普通跳 velocity.y = jump_velocity elif not has_double_jumped: # 在空中且未双跳过,执行双跳 velocity.y = double_jump_velocity has_double_jumped = true # 3. 获取水平输入(例如,绑定到A/D或左右箭头键) var direction = Input.get_axis("move_left", "move_right") if direction and not is_attacking: # 攻击时通常不能移动 velocity.x = direction * speed else: # 没有输入时,给一个平滑的减速,而不是瞬间停止 velocity.x = move_toward(velocity.x, 0, speed/10) # 4. 执行移动并处理碰撞 move_and_slide() # 5. 根据速度更新动画状态 update_animation(delta)这里有几个关键点:is_on_floor()是 CharacterBody2D 提供的方法,用于检测角色底部是否与“地板”碰撞层接触。move_and_slide()方法会根据我们计算好的velocity移动角色,并自动处理与碰撞体的滑动。move_toward()函数让角色的停止有一个渐缓过程,手感更自然。
4.3 动画状态机与视觉反馈
动画是让角色“活”起来的关键。我们将使用一个AnimationPlayer节点配合代码来控制动画。首先,在 AnimationPlayer 中创建几个动画:idle、run、jump、fall、attack。每个动画绑定到 AnimatedSprite2D 的animation属性上。
然后,我们编写update_animation函数:
func update_animation(delta): if is_attacking: $AnimationPlayer.play("attack") return # 攻击动画优先 if not is_on_floor(): # 空中状态 if velocity.y < 0: $AnimationPlayer.play("jump") else: $AnimationPlayer.play("fall") else: # 地面状态 if abs(velocity.x) > 1: $AnimationPlayer.play("run") # 根据方向翻转精灵 $AnimatedSprite2D.flip_h = velocity.x < 0 else: $AnimationPlayer.play("idle")注意,我们通过检查velocity.y的正负来判断是上升(跳)还是下降(落)。同时,水平移动时通过修改flip_h属性来让角色面朝移动方向,这比制作左右两套动画高效得多。
攻击逻辑可以通过一个独立的函数触发,并在攻击动画结束时通过 AnimationPlayer 的动画完成信号来重置is_attacking状态。
func _input(event): if event.is_action_pressed("attack") and not is_attacking: is_attacking = true $AnimationPlayer.play("attack") func _on_animation_player_animation_finished(anim_name): if anim_name == "attack": is_attacking = false5. 构建丛林世界:瓦片地图与关卡设计
5.1 使用 TileMap 节点创建可玩地形
Godot 的 TileMap 节点是构建 2D 关卡的神器。首先在场景中添加一个 TileMap 节点。然后,我们需要为其指定一个 TileSet 资源。在检查器中点击“瓦片集”属性,选择“新建瓦片集”。这会创建一个 .tres 资源文件,建议立即保存为“jungle_tileset.tres”。
双击这个资源或点击“编辑”按钮,会打开瓦片集编辑器。这里的关键是添加图源(Source)。点击“添加图源”,选择你的地形瓦片图片。导入后,Godot 会自动根据图片的透明背景进行分割。但更高效的做法是,如果你的素材是规整的网格图集,在“图源”配置中设置“纹理区域大小”(例如 16x16),然后点击“在场景中设置”,在纹理上框选所有需要的瓦片,一次性添加。
对于平台游戏,自动瓦片(Autotile)功能必不可少。它能让瓦片根据周围邻居自动选择正确的变体,快速绘制出连贯的地形。在瓦片集编辑器中,选择你刚添加的瓦片组,在“选中”模式下,点击“创建替代项”,然后选择“自动瓦片”。你需要定义哪些瓦片是“内部”的(如草地中心),哪些是“边角”的。Godot 4 的自动瓦片系统比 3.x 更强大但也更复杂,它使用“地形集”来定义连接规则。你需要为一种地形(如“泥土”)创建一个地形集,然后为瓦片的每个“位掩码”位置(周围8格)指定对应的瓦片变体。虽然设置稍显繁琐,但一旦配置好,绘制关卡的速度是飞跃性的。
5.2 分层设计与碰撞形状
一个真实的丛林不会只有一层地面。我们可以利用 TileMap 的**图层(Layer)**功能。例如:
- 图层 0:地形碰撞层。这一层放置所有玩家和敌人能站立、碰撞的固体瓦片。务必为这些瓦片在瓦片集编辑器中添加物理碰撞形状(通常是矩形)。
- 图层 1:背景装饰层。放置一些远处的树木、山峦背景,它们没有碰撞,只是视觉元素。
- 图层 2:前景细节层。放置一些藤蔓、小花、小石块等近处装饰,它们可能在碰撞层前面,增加层次感。
- 图层 3:危险层。专门放置尖刺、陷阱等,并为其分配一个不同的物理层(如“danger”),在碰撞处理中单独判断。
为瓦片添加碰撞形状是在瓦片集编辑器中完成的。选中一个瓦片,在右侧属性中找到“物理”部分,添加一个新的物理层,然后使用矩形或多边形工具绘制碰撞体。一个常见的坑是:碰撞形状画得太大,导致角色感觉被卡住。我的经验是,对于平台边缘,碰撞体可以比视觉瓦片向内收缩1-2个像素,这样角色从边缘落下会更顺畅。
5.3 关卡布局与场景组织
不要试图在一个巨大的 TileMap 上绘制整个游戏世界。Godot 在处理超大 TileMap 时可能会有效能问题。更好的做法是将关卡分割成多个房间或区域,每个区域是一个独立的场景。例如,“丛林入口.tscn”、“古老遗迹.tscn”、“瀑布深处.tscn”。然后在主场景(如“level_01.tscn”)中使用Node2D作为根节点,将这些区域场景作为子节点实例化进来,或者使用RemoteTransform2D等节点来在区域间无缝衔接。
对于相机控制,除了绑定在玩家身上的 Camera2D,你还可以使用Camera2D 的限位(Limit)属性,或者为其添加一个Rect2形状的VisibleOnScreenNotifier2D子节点,来确保相机不会移出关卡边界。更高级的做法是创建“相机轨道”,在关卡中设置一些路径点,让相机在玩家到达特定区域时平滑移动到预设位置,实现镜头引导。
6. 敌人与交互对象:让世界活起来
6.1 史莱姆:基础巡逻与碰撞敌人
敌人是冒险的调味剂。我们创建一个“slime.tscn”场景。它的根节点可以是CharacterBody2D(如果它有复杂移动)或Area2D(如果它是触碰即伤的类型)。对于经典的左右巡逻史莱姆,CharacterBody2D 更合适。
其脚本逻辑比玩家简单:
extends CharacterBody2D @export var patrol_speed: float = 50.0 @export var patrol_distance: float = 100.0 var start_position: Vector2 var target_position: Vector2 var moving_right: bool = true func _ready(): start_position = position target_position = start_position + Vector2(patrol_distance, 0) func _physics_process(delta): # 简单的左右移动 if moving_right: velocity.x = patrol_speed if position.x >= target_position.x: moving_right = false $Sprite2D.flip_h = true # 转身 else: velocity.x = -patrol_speed if position.x <= start_position.x: moving_right = true $Sprite2D.flip_h = false move_and_slide()为了检测与玩家的碰撞,我们可以在史莱姆场景中添加一个Area2D节点作为其“攻击范围”。当玩家的碰撞体进入这个区域时,Area2D 会发出body_entered信号,我们连接到脚本中对玩家造成伤害。
6.2 可收集物品:金币与生命值
可收集物品通常使用Area2D节点。创建一个“coin.tscn”场景,根节点为 Area2D,下面挂一个 Sprite2D(显示金币精灵)和一个 CollisionShape2D(用于检测区域)。
其脚本非常简单:
extends Area2D func _on_body_entered(body): if body.is_in_group("player"): # 确保只有玩家能收集 # 播放收集音效(如果有) $AudioStreamPlayer2D.play() # 隐藏视觉,防止重复收集 $Sprite2D.hide() $CollisionShape2D.set_deferred("disabled", true) # 通知游戏全局状态(如通过信号或全局单例) GameState.add_coins(1) # 等待音效播放完毕后删除节点 await $AudioStreamPlayer2D.finished queue_free()这里有几个技巧:1. 使用is_in_group(“player”)来精确判断碰撞体是玩家。你需要在玩家场景的根节点上将其加入“player”组。2. 使用set_deferred(“disabled”, true)来安全地在物理回调中禁用碰撞体。3. 使用await等待音效播放完毕再删除节点,避免音效被中途切断。
6.3 触发器与机关:压力板与移动平台
机关能大大增加关卡趣味性。一个简单的压力板可以用Area2D实现。当玩家站上去时,压力板下沉并播放一个动画,同时发出信号,触发与之相连的机关(如打开一扇门、激活一个移动平台)。
移动平台则更复杂一些。一个常见的实现方式是使用Path2D和PathFollow2D节点。先绘制一条路径(Path2D),然后将一个 StaticBody2D(作为平台视觉和碰撞体)作为 PathFollow2D 的子节点。在 PathFollow2D 的脚本中,让其progress_ratio属性随时间循环变化,平台就会沿着路径移动。为了让玩家能站在移动平台上,需要将平台的碰撞层设置为与玩家交互,并且在玩家脚本中,当检测到站在平台上时,将玩家的全局位置附加到平台的移动上。这可以通过在_physics_process中检查get_floor_velocity()来实现,CharacterBody2D 会自动处理一部分,但复杂移动可能需要手动将平台速度加到玩家速度上。
7. 游戏逻辑与用户界面
7.1 全局游戏状态管理:单例模式
我们需要一个地方来集中管理游戏状态,比如玩家的金币数、生命值、当前关卡等。在 Godot 中,这通常通过自动加载单例(AutoLoad Singleton)实现。
创建一个名为“GameState.gd”的脚本:
extends Node var coins: int = 0 var health: int = 3 var max_health: int = 5 var current_level: String = “res://levels/level_01.tscn” signal coins_changed(new_value) signal health_changed(new_value) signal player_died func add_coins(amount: int): coins += amount coins_changed.emit(coins) func take_damage(amount: int): health -= amount health_changed.emit(health) if health <= 0: player_died.emit() # 处理游戏结束逻辑,如重启关卡 reset_level() func reset_level(): # 重置玩家状态,重新加载场景等 coins = 0 health = max_health get_tree().change_scene_to_file(current_level)然后,在“项目 -> 项目设置 -> 自动加载”中,将这个脚本的路径添加进去,并给它起个名字,比如“GameState”。这样,在任何场景的任何脚本中,你都可以通过GameState.add_coins(1)这种方式来访问和修改全局状态。
7.2 UI 界面:生命值、金币与菜单
Godot 的 UI 系统基于 Control 节点。我们创建一个“ui.tscn”场景,根节点为CanvasLayer,这能确保 UI 始终显示在最上层。在其下添加各种 Control 节点。
对于生命值显示,一个常见的做法是使用一排TextureRect节点来表示心形图标。在 UI 脚本中,监听 GameState 的health_changed信号:
extends CanvasLayer @onready var hearts_container = $HeartsContainer func _ready(): # 初始化生命显示 update_hearts(GameState.health) # 连接信号 GameState.health_changed.connect(update_hearts) func update_hearts(current_health): for i in hearts_container.get_child_count(): var heart = hearts_container.get_child(i) # 假设每个 TextureRect 有两个纹理:满心和空心 if i < current_health: heart.texture = preload(“res://assets/ui/heart_full.png”) else: heart.texture = preload(“res://assets/ui/heart_empty.png”)金币显示可以用一个Label节点,同样监听coins_changed信号来更新文本。
暂停菜单和游戏结束菜单可以设计为独立的场景。通过设置get_tree().paused = true来暂停游戏物理和逻辑进程,然后弹出菜单场景。记得在菜单中提供继续、重试、返回主标题的按钮,并处理好相应的场景切换和状态重置。
7.3 场景切换与持久化
关卡之间的切换使用get_tree().change_scene_to_file(“res://path/to/next_level.tscn”)。为了在切换时保留一些状态(如金币数),就需要依赖我们之前创建的 GameState 单例。
如果想让玩家可以从上次的关卡继续,就需要实现简单的数据持久化。Godot 提供了ConfigFile类,可以方便地读写 ini 格式的文件。
# 在 GameState.gd 中增加保存/加载功能 var config = ConfigFile.new() func save_game(): config.set_value(“player”, “coins”, coins) config.set_value(“player”, “health”, health) config.set_value(“game”, “current_level”, current_level) config.save(“user://savegame.cfg”) func load_game(): var err = config.load(“user://savegame.cfg”) if err == OK: coins = config.get_value(“player”, “coins”, 0) health = config.get_value(“player”, “health”, max_health) current_level = config.get_value(“game”, “current_level”, “res://levels/level_01.tscn”)user://是 Godot 指定的用户数据目录,跨平台兼容。可以在游戏主菜单调用load_game(),在通关或退出时调用save_game()。
8. 音效、粒子与游戏优化
8.1 音频系统的集成
没有声音的游戏是没有灵魂的。Godot 使用AudioStreamPlayer(2D 空间音效)和AudioStreamPlayer2D(与场景中位置相关的音效)节点。对于背景音乐,通常用一个全局的 AudioStreamPlayer 单例来管理,防止场景切换时音乐中断。
为玩家的跳跃、攻击、收集金币、受伤等动作添加音效。一个高效的做法是创建一个“SoundManager”单例,它预加载所有常用的音效 AudioStream,并提供简单的播放接口。
# SoundManager.gd (作为单例自动加载) extends Node var sounds: Dictionary = {} func _ready(): # 预加载音效 sounds[“jump”] = preload(“res://assets/sfx/jump.wav”) sounds[“coin”] = preload(“res://assets/sfx/coin.wav”) # … 加载其他音效 func play_sound(sound_name: String, volume_db: float = 0.0): if sounds.has(sound_name): var player = AudioStreamPlayer.new() add_child(player) player.stream = sounds[sound_name] player.volume_db = volume_db player.play() await player.finished player.queue_free()这样,在任何地方只需要SoundManager.play_sound(“jump”)即可播放音效,无需在每个场景中创建播放器节点。
8.2 粒子效果提升表现力
粒子效果(GPUParticles2D)能极大提升游戏质感,比如角色跳跃时的尘土、收集物品时的闪光、敌人被击败时的爆炸。
Godot 的粒子编辑器非常直观。创建一个 GPUParticles2D 节点,在检查器中可以调整其数量、生命周期、速度、颜色渐变等几乎所有参数。对于跳跃尘土,可以设置一个向四周扩散的圆锥形发射器,使用棕色系的颜色,并让粒子快速生成然后消失。一个重要的技巧是:将粒子的“局部坐标”属性关闭,这样粒子发射后就不再跟随父节点移动,否则会拖出长长的轨迹。
对于需要精确控制发射时机的效果(如攻击命中火花),可以在代码中通过$Particles2D.emitting = true来触发。记得将粒子系统的“一次性”属性勾选上,这样它发射完预设数量的粒子后就会自动停止。
8.3 性能分析与优化技巧
即使是一个简单的 2D 游戏,优化也能让它在低端设备上更流畅。Godot 内置了性能监视器(调试器 -> 监视器),可以实时查看帧率(FPS)、绘制调用(Draw Calls)、物理步骤时间等关键指标。
优化绘制调用:2D 游戏性能的常见瓶颈。每个不同的纹理、材质、着色器实例都可能增加一次绘制调用。对于像素游戏,尽量使用纹理图集(Texture Atlas),将多个小精灵打包到一张大图上。Godot 在导入设置中可以选择“纹理 -> 导入为 -> Texture2DAtlas”,但更推荐在外部工具(如 TexturePacker)中打包好再导入。使用 TileMap 本身也是批量绘制,能极大减少绘制调用。
节点数量管理:场景中节点过多也会影响性能。对于大量重复的、动态生成的对象(如子弹、特效),使用节点池(Object Pooling)技术。即预先实例化一定数量的对象并隐藏,需要时激活并显示,用完后再隐藏放回池中,而不是频繁地instance()和queue_free()。
物理优化:确保碰撞形状尽可能简单(矩形、胶囊形优于复杂多边形)。对于不会移动的静态地形,使用StaticBody2D。对于大量简单的、不需要精确碰撞的物体(如金币),可以考虑使用Area2D代替RigidBody2D或CharacterBody2D。
使用可见性通知器:对于大型关卡,为屏幕外的敌人、机关等添加VisibleOnScreenNotifier2D节点。当它们离开屏幕时,通过脚本将其process_mode设置为PROCESS_MODE_DISABLED或直接隐藏,可以节省大量计算资源。
最后,一定要在目标平台或较低配置的电脑上进行测试。编辑器里流畅不代表在实机上也能流畅。Godot 的导出模板非常高效,通过“项目 -> 导出”可以轻松打包到 Windows、macOS、Linux、甚至 Web 和移动端,跨平台测试是发布前的必修课。