
1. 项目概述从零到一用Godot引擎打造你的首款2D游戏如果你一直对游戏开发充满好奇想亲手制作一款属于自己的游戏但又觉得Unity、Unreal Engine这些“巨无霸”引擎门槛太高或者被复杂的C、C#劝退那么Godot引擎可能就是为你量身定做的。作为一个开源、免费、轻量且功能强大的游戏引擎Godot以其独特的场景-节点架构和易上手的GDScript语言让独立开发者和初学者也能快速上手。今天我们就以制作一个经典的“躲避怪物”2D游戏为例手把手带你完成从项目创建到打包分享的全过程。这个项目麻雀虽小五脏俱全涵盖了2D游戏开发的核心流程角色控制、敌人生成、碰撞检测、UI界面和游戏状态管理。无论你是编程新手还是想从其他引擎转战过来的开发者这篇教程都将为你铺平道路。2. 项目初始化与结构规划2.1 引擎安装与项目创建首先你需要从Godot官网下载并安装最新稳定版的引擎。启动Godot后你会看到项目管理器。点击“新建项目”给它起个名字比如“DodgeTheCreeps”。选择一个空文件夹作为项目路径。在渲染器选择上对于2D游戏移动端渲染器Mobile和向前渲染器Forward在性能上差异不大但移动端渲染器对低端设备更友好我们这里保持默认即可。创建项目后你会进入主编辑器界面。Godot采用独特的基于场景Scene和节点Node的架构。你可以把场景理解为一个容器或关卡而节点则是构成这个场景的所有元素比如精灵、碰撞体、脚本等。它们以树状结构组织这就是“场景树”。2.2 资源管理与文件夹结构一个清晰的项目结构是良好开发习惯的开端。在左下角的“文件系统”面板中我们手动创建几个文件夹来管理资源art/: 存放所有图像、精灵图等美术资源。scripts/: 存放所有的GDScript脚本文件。scenes/: 存放所有的场景文件.tscn。audio/: 存放音效和背景音乐。将你下载的素材包包含玩家、敌人、背景等图片解压把图片拖入art/文件夹。Godot会自动导入这些图片资源。对于像素风游戏一个关键设置是避免纹理过滤导致的模糊。选中导入的图片在右侧“导入”面板中将“模式”从“纹理”改为“2D像素”并关闭“Mipmaps”和“Filter”。这样能确保像素边缘清晰锐利。3. 核心场景构建玩家与敌人3.1 创建玩家Player场景我们的游戏主角——玩家本身就是一个独立的、可复用的场景。点击“场景”菜单 - “新建场景”。我们从一个CharacterBody2D节点开始并将其重命名为“Player”。CharacterBody2D是专门为需要自定义移动逻辑如平台跳跃、顶头碰撞的角色设计的物理体比RigidBody2D完全受物理模拟控制更易操控。接下来为Player节点添加子节点Sprite2D: 用于显示玩家形象。在检查器中将“纹理”属性指向art/player.png。CollisionShape2D: 用于定义玩家的碰撞区域。为其添加一个RectangleShape2D并调整矩形大小使其大致匹配精灵图像的轮廓。碰撞形状不宜过大或过小略小于精灵视觉范围是常见做法能让游戏感觉更公平。Camera2D可选但推荐作为玩家的子节点可以让镜头跟随玩家移动。勾选“正在运行”属性并适当调整“缩放”和“拖拽边距”以获得舒适的视野。现在场景树看起来应该是这样的Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── Camera2D将这个场景保存为scenes/player.tscn。3.2 编写玩家移动脚本选中Player根节点点击检查器顶部的“添加脚本”按钮。语言选择GDScript保存到scripts/player.gd。玩家的核心逻辑是读取键盘输入计算移动方向然后应用移动并处理碰撞。CharacterBody2D提供了move_and_slide()方法它能自动处理沿斜坡滑动和碰撞停止。extends CharacterBody2D # 定义玩家移动速度可以通过编辑器调整 export var speed: int 400 func _physics_process(delta: float): # 获取输入向量范围在[-1, 1]之间 var direction Input.get_vector(move_left, move_right, move_up, move_down) # 计算速度向量。如果方向不为零则归一化并乘以速度否则速度为零。 if direction.length() 0: velocity direction.normalized() * speed else: velocity Vector2.ZERO # 使用move_and_slide移动玩家。它返回碰撞后剩余的速度但我们这里不需要。 move_and_slide()这里用到了Input.get_vector它需要一个预先定义好的输入映射。我们需要去“项目设置” - “输入映射”中添加四个动作move_left,move_right,move_up,move_down并分别绑定到A/D/左箭头/右箭头和W/S/上箭头/下箭头。实操心得在_physics_process中处理移动逻辑而不是在_process中。因为_physics_process的调用频率与物理引擎同步默认每秒60次能保证移动和碰撞检测的稳定性避免因帧率波动导致角色“穿墙”。3.3 创建敌人Mob场景敌人同样是一个可重复生成的场景。新建一个场景根节点使用RigidBody2D命名为“Mob”。RigidBody2D适合不需要复杂控制、只需受物理影响的物体比如被玩家撞飞虽然我们这里不需要或自然下落的敌人。为Mob节点添加子节点Sprite2D: 纹理指向art/mob.png。CollisionShape2D: 同样添加一个RectangleShape2D或CapsuleShape2D如果敌人是圆形的。VisibleOnScreenNotifier2D: 这是一个非常有用的节点。当它离开屏幕视口时会发出screen_exited信号。我们可以利用这个信号来销毁离开屏幕的敌人防止它们无限累积消耗性能。保存场景为scenes/mob.tscn。为Mob添加脚本scripts/mob.gd。敌人的逻辑是在屏幕上方随机位置生成并以随机速度向下或斜向移动。extends RigidBody2D # 定义敌人移动的最小和最大速度范围 export var min_speed: float 150.0 export var max_speed: float 250.0 func _ready(): # 为敌人随机选择一个动画精灵如果有多个变体的话 # 这里假设我们只有一个精灵所以先注释掉 # var mob_types $AnimatedSprite2D.sprite_frames.get_animation_names() # $AnimatedSprite2D.play(mob_types[randi() % mob_types.size()]) func _on_visible_on_screen_notifier_2d_screen_exited(): # 当敌人离开屏幕时安全地销毁自身实例 queue_free()敌人如何开始移动并设置方向呢我们将在主场景中生成敌人时通过函数参数来初始化它们。4. 游戏主场景与逻辑控制4.1 搭建主场景Main主场景是游戏的舞台和大脑。新建场景根节点使用Node2D命名为“Main”。然后添加以下节点Player节点不是直接创建而是点击“实例化子场景”按钮选择我们之前保存的player.tscn。将其放置在屏幕底部中央。Timer节点命名为MobTimer用于定期生成敌人。将其“等待时间”设为1秒并勾选“自动开始”。Timer节点命名为ScoreTimer用于每秒增加分数。Timer节点命名为StartTimer用于游戏开始前的倒计时。Marker2D节点命名为StartPosition用于标记玩家初始位置。Path2D节点命名为MobPath。在其下添加一个PathFollow2D子节点命名为MobSpawnLocation。我们将用这个路径节点来定义敌人可能的生成位置。在2D视图中用曲线工具绘制一条横跨屏幕上方的水平线或波浪线。PathFollow2D可以沿着这条路径移动并且可以设置一个随机的“进度比”这样我们就能在路径上的任意点生成敌人了。保存场景为scenes/main.tscn。4.2 编写主场景脚本为主场景根节点添加脚本scripts/main.gd。这里将集中处理游戏的核心逻辑开始游戏、生成敌人、更新分数和结束游戏。extends Node2D # 通过export将敌人场景预加载方便在编辑器中赋值 export var mob_scene: PackedScene # 引用场景中的节点 onready var player $Player onready var mob_timer $MobTimer onready var score_timer $ScoreTimer onready var start_timer $StartTimer onready var mob_spawn_location $MobPath/MobSpawnLocation onready var hud $HUD # 我们稍后会创建HUD场景 var score: int 0 func _ready(): # 初始时连接各个计时器的timeout信号到对应的函数 mob_timer.timeout.connect(_on_mob_timer_timeout) score_timer.timeout.connect(_on_score_timer_timeout) start_timer.timeout.connect(_on_start_timer_timeout) # 假设HUD有一个“开始游戏”按钮按下后触发此函数 hud.start_game.connect(new_game) func new_game(): # 重置分数并显示 score 0 hud.update_score(score) hud.show_message(Get Ready!) # 将玩家重置到起始位置 player.position $StartPosition.position player.show() # 确保玩家可见 # 开始计时器 start_timer.start() # 等待StartTimer结束后再开始生成敌人和计分 # 这个逻辑在_on_start_timer_timeout中处理 func _on_start_timer_timeout(): # 开始游戏主循环 mob_timer.start() score_timer.start() func _on_mob_timer_timeout(): # 生成一个敌人实例 var mob mob_scene.instantiate() # 在路径上随机选择一个生成点 mob_spawn_location.progress_ratio randf() var spawn_position mob_spawn_location.position # 设置敌人的初始位置和方向 mob.position spawn_position # 让敌人朝向玩家方向或随机方向移动 var direction (player.position - spawn_position).normalized() # 添加一些随机性让方向不那么直接 direction direction.rotated(randf_range(-PI / 4, PI / 4)) # 设置敌人的线速度和角速度旋转 mob.linear_velocity direction * randf_range(mob.min_speed, mob.max_speed) mob.angular_velocity randf_range(-PI, PI) # 随机旋转 # 将敌人添加到主场景中 add_child(mob) func _on_score_timer_timeout(): # 每秒增加分数 score 1 hud.update_score(score) func game_over(): # 停止所有计时器 mob_timer.stop() score_timer.stop() # 显示游戏结束信息 hud.show_game_over()这里有几个关键点信号连接我们使用GDScript的.connect()方法将计时器的timeout信号连接到对应的函数。这是一种Godot推崇的松耦合通信方式。实例化场景mob_scene.instantiate()从我们预加载的PackedScene资源中创建一个新的Mob实例。随机生成通过设置PathFollow2D的progress_ratio为随机值我们在预设路径上获得了一个随机生成点。方向计算让敌人大致朝向玩家移动但加入随机旋转角使移动路径不可预测增加游戏难度。注意事项在_ready()函数中获取节点引用时使用onready关键字可以确保在节点完全进入场景树后再进行赋值这是一种更安全、更清晰的做法避免了在_ready函数内写一长串$NodePath。5. 用户界面HUD与游戏反馈5.1 创建HUD场景游戏需要显示分数、消息如“Get Ready!”、“Game Over”和一个开始按钮。新建一个场景根节点使用CanvasLayer命名为“HUD”。CanvasLayer的绘制顺序独立于主世界适合UI元素。在CanvasLayer下添加Label节点命名为ScoreLabel用于显示分数。将其锚点Anchor设置为右上角调整边距选择一个大而清晰的字体。Label节点命名为MessageLabel用于显示临时消息如“Get Ready!”。将其放在屏幕中央使用更大的字体。Button节点命名为StartButton文本设为“Start”。将其放在屏幕中央偏下的位置。Timer节点命名为MessageTimer用于控制消息显示的时长。保存为scenes/hud.tscn。5.2 编写HUD脚本为HUD根节点添加脚本scripts/hud.gd。extends CanvasLayer # 声明信号用于通知主场景游戏开始 signal start_game # 引用子节点 onready var score_label: Label $ScoreLabel onready var message_label: Label $MessageLabel onready var start_button: Button $StartButton onready var message_timer: Timer $MessageTimer func _ready(): # 初始隐藏消息 message_label.hide() # 连接按钮按下信号和消息计时器超时信号 start_button.pressed.connect(_on_start_button_pressed) message_timer.timeout.connect(_on_message_timer_timeout) func update_score(new_score: int): score_label.text Score: %s % new_score func show_message(text: String): message_label.text text message_label.show() message_timer.start() # 2秒后自动隐藏 func show_game_over(): show_message(Game Over) # 游戏结束后稍等片刻再显示开始按钮 await get_tree().create_timer(1.0).timeout start_button.show() func _on_start_button_pressed(): start_button.hide() start_game.emit() # 发出开始游戏信号 func _on_message_timer_timeout(): message_label.hide()5.3 连接HUD与主场景回到main.tscn场景实例化hud.tscn作为Main的子节点。确保在main.gd脚本中正确获取了$HUD的引用。现在需要在主场景的_ready()函数中连接HUD发出的start_game信号func _ready(): # ... 其他连接 hud.start_game.connect(new_game) # 这行已在上文代码中同时修改game_over()函数调用HUD的show_game_over方法。6. 碰撞检测与游戏状态6.1 玩家与敌人的碰撞我们的游戏规则是玩家碰到敌人游戏结束。这需要碰撞检测。在player.tscn中为Player节点添加一个Area2D子节点命名为“HitBox”。为这个Area2D也添加一个CollisionShape2D形状可以比玩家的碰撞体稍大一点这样能让游戏体验更宽松一些。为Area2D连接body_entered信号到Player脚本。这个信号会在任何物理体RigidBody2D, CharacterBody2D, StaticBody2D进入该区域时触发。在player.gd中添加func _on_hit_box_body_entered(body: Node2D): # 当有物体敌人进入碰撞区域时隐藏玩家并发出“被击中”信号 hide() # 可以在这里播放一个被击中的音效或动画 # $HitSound.play() hit.emit() # 我们需要先定义一个信号 # 在脚本顶部定义信号 signal hit然后在主场景main.gd中我们需要连接玩家的hit信号func _ready(): # ... 其他连接 player.hit.connect(game_over) # 玩家被击中时游戏结束在game_over函数中除了停止计时器和显示HUD还应该调用player.hide()并可能重置玩家的位置。6.2 游戏开始与重置完整的游戏流程应该是点击HUD的Start按钮 - 玩家出现在起始点 - 开始倒计时 - 敌人开始生成并计分 - 玩家被击中 - 显示Game Over并出现Start按钮。我们需要确保每次新游戏开始时清理掉场上所有残留的敌人。在main.gd的new_game函数中添加func new_game(): # 清理上一局残留的敌人 get_tree().call_group(mobs, queue_free) # ... 其他重置代码这里用到了“组”Group功能。我们需要为每个生成的敌人都添加到一个叫“mobs”的组里。在mob.gd的_ready()函数中加入func _ready(): add_to_group(mobs) # ... 其他初始化call_group方法会调用该组内所有节点的指定方法这里就是安全地销毁所有敌人。7. 打磨与优化音效、背景与发布7.1 添加音效和背景音乐音效能极大提升游戏体验。在art/或新建的audio/文件夹中导入背景音乐如background_music.ogg和音效如hit_sound.wav,score_sound.wav。背景音乐在主场景Main中添加一个AudioStreamPlayer节点将其流Stream属性指向背景音乐文件勾选“自动播放”和“循环”。得分音效在HUD场景中添加一个AudioStreamPlayer节点用于播放得分音效。在hud.gd的update_score函数中调用$ScoreSound.play()。被击中和开始音效可以分别添加到Player和HUD场景中在对应事件触发时播放。7.2 设置游戏背景和边界为了让游戏世界更完整我们可以添加一个背景精灵和一个限制玩家移动的边界。背景在main.tscn中添加一个Sprite2D作为Main的第一个子节点这样它会在最底层纹理指向背景图片。可以将其缩放至铺满整个视口。边界添加一个StaticBody2D节点为其添加四个CollisionShape2D使用SegmentShape2D或RectangleShape2D来构成屏幕的四条边界。这样玩家和敌人就不会跑出屏幕外了。对于敌人我们已经有VisibleOnScreenNotifier2D来销毁离开屏幕的所以边界主要是为了限制玩家。7.3 项目设置与导出在发布游戏前需要配置一些项目设置项目 - 项目设置应用/运行设置主场景为scenes/main.tscn。显示/窗口设置初始窗口大小如1024x600并可以取消“可调整大小”选项。输入映射确保我们之前定义的move_left等动作已正确设置。最后点击“项目” - “导出”添加一个导出预设如“Windows Desktop”。你需要下载对应的导出模板。然后选择一个导出路径点击“导出项目”。Godot会生成一个独立的可执行文件你可以分享给朋友试玩了8. 常见问题与排查技巧实录在按照教程操作时你可能会遇到一些典型问题。这里我总结了一份速查表问题现象可能原因解决方案玩家或敌人图片显示为粉色方块图片导入设置错误或路径不对检查文件系统面板中图片是否正常检查Sprite2D的“纹理”属性路径是否正确。确保图片导入模式为“2D像素”如果是像素画。玩家移动没有反应1. 输入映射未设置。2. 脚本未正确附加或代码有语法错误。3. 速度(speed)值设置过小。1. 检查“项目设置”-“输入映射”中动作是否绑定按键。2. 检查Player节点是否附加了player.gd脚本并查看“输出”面板是否有脚本错误。3. 在编辑器中调整Player节点的speed导出变量试试。敌人不生成1.MobTimer未启动或等待时间过长。2.mob_scene导出变量未赋值。3. 生成位置计算错误。1. 检查Main场景中MobTimer的“自动开始”是否勾选等待时间是否合理。2. 在Main场景的检查器中将Mob属性指向scenes/mob.tscn。3. 打印spawn_position调试或临时将敌人位置固定在屏幕中央测试。碰撞检测不生效1. 碰撞层Layer和遮罩Mask未设置。2. 碰撞形状大小或位置不正确。3. 信号未正确连接。1. 确保Player的HitBoxArea2D的“遮罩”层勾选了敌人所在的层默认为第1层反之亦然。2. 在场景编辑器中可视化碰撞形状按F3确保它们有重叠。3. 检查Area2D的body_entered信号是否连接到Player脚本的正确函数。游戏结束后点击Start按钮无反应HUD的start_game信号未连接到Main脚本的new_game函数。在Main场景中选中HUD节点在检查器的“节点”选项卡中确保start_game信号已连接到Main脚本的new_game方法。导出后游戏运行崩溃或资源丢失导出时未包含所有必要文件。在导出窗口的“资源”选项卡中确保模式是“导出所有资源”。检查“导出路径”中的文件是否包含了所有场景和图片。独家避坑技巧善用“远程”场景树在编辑器运行游戏时切换到“远程”选项卡可以实时查看运行中的场景树和节点属性是调试节点层级和属性值的利器。打印调试信息在关键函数中使用print()或print_debug()输出变量值这是定位逻辑错误最简单直接的方法。使用“导出”变量像speed、min_speed这类需要频繁调整的参数一定要用export暴露在编辑器检查器中。这样你可以在不修改代码的情况下实时调整数值并看到游戏内的即时变化极大地提升了调试和平衡游戏性的效率。理解_process与_physics_process记住所有与物理、移动、碰撞相关的逻辑都应放在_physics_process中。而UI更新、非物理动画等则可以放在_process中。混用可能导致奇怪的抖动或碰撞不一致。完成以上所有步骤你就拥有了一款功能完整的2D躲避游戏。这个项目虽然基础但它几乎触及了Godot 2D游戏开发的所有核心概念场景组织、节点通信信号、物理交互、随机生成、UI构建和状态管理。你可以以此为基础添加更多功能比如不同种类的敌人、道具、关卡、更复杂的玩家能力如冲刺、射击甚至联网功能。Godot的潜力远不止于此希望这个“第一次”能成为你精彩游戏开发生涯的坚实起点。