- 示例工程
【免费下载链接】godot-demo-projects
Demonstration and Template Projects
导读
本文以 2d/physics_platformer 官方演示项目为蓝本,完整讲解如何用RigidBody2D(刚体)构建 2D 平台跳跃游戏的角色控制器。与常见的CharacterBody2D(运动体)方案不同,刚体角色天然融入物理引擎,可以实现跷跷板、骑乘敌人、被移动平台带动等真实交互,代价是所有移动逻辑必须写在_integrate_forces()回调中手动修改刚体速度。读完本文,你将掌握刚体角色控制器的参数体系、地板判定与速度传导、敌人 AI 与子弹交互、物理关卡搭建等完整实战方案,并了解该项目在 project.godot 中的配套物理设置。
项目概览:一个完全基于物理的角色平台跳跃演示
Physics Platformer(项目名 "Physics-Based Platformer 2D",见 project.godot)是 godot-demo-projects 仓库 2D 分类下的官方演示,核心卖点正如其 README 所述:玩家与敌人全部使用RigidBody2D动态角色控制器,它们能"完美地与物理交互"——场景里有可踩踏的跷跷板(seesaw),玩家甚至可以跳到敌人头上"骑"着它移动。
该演示使用 GDScript 编写,渲染器为 Compatibility(renderer/rendering_method="gl_compatibility"),关卡主场景为 stage.tscn,窗口分辨率 800×480。场景中通过实例化(instancing)把 player/player.tscn、enemy/enemy.tscn、coin/coin.tscn 以及各种平台道具组合到同一关卡,充分体现了"场景独立编辑、关卡统一实例化"的工程组织方式(stage.tscn 开头的 Label 提示文字也明确说明了这一点)。
为什么用 RigidBody2D 而不是 CharacterBody2D
README 明确指出:刚体角色控制器比CharacterBody2D更强大,但更难以处理,因为它要求手动修改刚体的速度。
| 维度 | RigidBody2D(本演示) | CharacterBody2D |
|---|---|---|
| 移动方式 | 在_integrate_forces()中读写PhysicsDirectBodyState2D的线性速度 | move_and_slide()直接移动并自动处理碰撞 |
| 物理交互 | 原生参与刚体模拟,可被推、可压在跷跷板上、可被移动平台带动 | 运动体与刚体交互需额外配置(move_and_slide默认不推动刚体) |
| 旋转控制 | 默认会翻滚,需lock_rotation | 默认不旋转 |
| 重力处理 | 需要手动叠加get_total_gravity() | 自动受重力影响(除非gravity_scale = 0) |
| 适用场景 | 强调真实物理反馈的玩法(跷跷板、骑乘、弹射) | 传统平台跳跃、追求精确控制的角色 |
从 player.tscn 可以看到刚体角色的关键配置:mass = 1.5、physics_material_override摩擦为 0(避免地面摩擦拖慢移动)、lock_rotation = true(锁定旋转防止倒地翻滚)、custom_integrator = true(由脚本接管积分逻辑,配合_integrate_forces)、contact_monitor = true+max_contacts_reported = 4(开启接触监控用于地板检测)。
核心机制:一切移动都在 _integrate_forces() 中完成
README 强调:所有移动必须与物理引擎同步,在_integrate_forces()内部进行。这是刚体角色控制器与传统脚本控制的本质区别——你不能在_process/_physics_process里直接设置刚体位置或速度,因为物理引擎每帧会基于受力重新求解刚体状态,直接赋值会被覆盖且造成物理不同步。
player/player.gd 展示了标准写法:
func _integrate_forces(state: PhysicsDirectBodyState2D) -> void: var velocity := state.get_linear_velocity() var step := state.get_step() # 本帧物理步长 # 1. 读取输入 var move_left := Input.is_action_pressed(&"move_left") var move_right := Input.is_action_pressed(&"move_right") var jump := Input.is_action_pressed(&"jump") # 2. 计算目标水平速度(地面/空中分别处理) # 3. 手动叠加重力 velocity += state.get_total_gravity() * step # 4. 写回刚体 state.set_linear_velocity(velocity)核心要点:
- 通过
state而非节点属性读写速度:state.get_linear_velocity()/state.set_linear_velocity(),配合custom_integrator = true,脚本完全接管移动积分; - 手动叠加重力:
state.get_total_gravity()返回项目级重力矢量(本项目为 900),乘以step后累加,这是 README 所说"手动修改速度"最直接的一步; - 使用
step而非delta:state.get_step()是物理步长(项目配置 120 ticks/s 时为 1/120 秒),确保速度换算与物理引擎完全一致; call_deferred处理场景变更:生成子弹、生成敌人等会修改场景树的操作,通过_shot_bullet.call_deferred()/_spawn_enemy_above.call_deferred()延迟执行,避免在物理回调中修改场景造成冲突。
玩家控制详解:参数体系与手感调校
玩家控制器由一系列常量集中定义手感参数,位于 player/player.gd:
| 常量 | 值 | 作用 |
|---|---|---|
WALK_ACCEL | 1000.0 | 地面水平加速度 |
WALK_DEACCEL | 1000.0 | 地面水平减速度(松键减速) |
WALK_MAX_VELOCITY | 200.0 | 地面最大水平速度 |
AIR_ACCEL | 250.0 | 空中水平加速度(远小于地面,体现"空中操控差"的手感) |
AIR_DEACCEL | 250.0 | 空中水平减速度 |
JUMP_VELOCITY | 380.0 | 跳跃初速度(向上为负) |
STOP_JUMP_FORCE | 450.0 | 松键跳衰减力(实现可变跳高度) |
MAX_SHOOT_POSE_TIME | 0.3 | 射击姿势动画保持时长 |
MAX_FLOOR_AIRBORNE_TIME | 0.15 | 地板判定容忍时间(跳跃缓冲/物理误差补偿) |
地面与空中的差异化处理
在_integrate_forces()中,代码根据是否在地板上分支处理水平移动(player/player.gd):
- 地面移动:按住方向键时按
WALK_ACCEL * step累加速度并钳制在 ±WALK_MAX_VELOCITY;未按键时用WALK_DEACCEL逐步衰减到 0; - 空中移动:同样结构但使用小得多的
AIR_ACCEL/AIR_DEACCEL(250 vs 1000),使玩家在空中转向迟缓、更真实; - 跳跃:在地面且按下跳跃时一次性赋予
velocity.y = -JUMP_VELOCITY(-380,向上);若跳跃中松键,则通过STOP_JUMP_FORCE * step向上加速度持续抵消上升速度,实现按住跳得高、轻点跳得矮的可变跳跃; - 可变跳跃状态机:
jumping、stopping_jump两个布尔配合管理跳起、衰减、下落三个阶段的转换,并伴随sound_jump.play()播放跳跃音效(player/player.gd)。
地板检测:基于接触法线的判定
刚体没有is_on_floor()可用,因此玩家通过遍历接触点、检查碰撞法线方向来判断是否"站在地板上"(player/player.gd):
for contact_index in state.get_contact_count(): var collision_normal := state.get_contact_local_normal(contact_index) if collision_normal.dot(Vector2(0, -1)) > 0.6: found_floor = true floor_index = contact_index法线与竖直向上方向(0, -1)的点积大于 0.6,即法线基本朝上,就视为地板接触。配合MAX_FLOOR_AIRBORNE_TIME = 0.15秒的"空中宽容期"(airborne_time < 0.15仍算站在地面),可以补偿物理引擎的微小误差和人类反应延迟——这正是 README 与代码注释共同强调的刚体角色实现技巧(player/player.gd)。
移动平台与地板速度传导
这是一个刚体角色特有的关键细节:当玩家站在移动平台上时,必须把平台的水平速度叠加到玩家速度上,否则会被平台"甩下"。
代码用floor_h_velocity变量实现(player/player.gd):
# 每帧先移除上一帧传导的平台速度,再重新计算 velocity.x -= floor_h_velocity floor_h_velocity = 0.0 ... if found_floor: floor_h_velocity = state.get_contact_collider_velocity_at_position(floor_index).x velocity.x += floor_h_velocitystate.get_contact_collider_velocity_at_position(floor_index)直接读取所站碰撞体的水平速度并叠加到自己身上,于是玩家能稳稳站在平台上随其移动。项目中的移动平台本身是CharacterBody2D(见 platform/moving_platform.tscn),由 stage.tscn 中的AnimationPlayer(autoplay "move" 动画,时长 6 秒)驱动,3 块平台在关卡中按设定路径往复运动。
敌人 AI:状态机 + 射线 + 子弹交互
敌人同样是RigidBody2D(enemy/enemy.gd),其 AI 展示了刚体角色如何配合RayCast2D做寻路与转向:
状态机:WALKING / DYING
enum State { WALKING, DYING, }- WALKING:设置
velocity.x = direction * WALK_SPEED(WALK_SPEED = 50,enemy/enemy.gd),并根据两类检测翻转方向:- 接触法线检测:遍历接触点,若水平法线分量绝对值大于 0.9(说明撞墙)则反向(enemy/enemy.gd);
- 悬空检测:左右各挂一个
RayCast2D(rc_left/rc_right),当行进方向前方脚下没有碰撞体(not rc_left.is_colliding())而另一侧有碰撞时,判定即将掉下平台边缘,立即掉头(enemy/enemy.gd),同时翻转 Sprite 的scale.x实现朝向切换;
- DYING:播放 "explode" 动画,通过
_pre_explode()移除 3 个碰撞形状防止后续碰撞,并播放爆炸音效,最终_die()调用queue_free()释放(enemy/enemy.gd)。
子弹交互与"死亡旋转"
在_integrate_forces中敌人检测接触对象是否为Bullet(enemy/enemy.gd):
var collider := state.get_contact_collider_object(collider_index) if collider is Bullet and not (collider as Bullet).disabled: _bullet_collider.call_deferred(collider, state, collision_normal) break命中后(enemy/enemy.gd):
- 状态切到
DYING; - 根据碰撞法线水平分量的符号,设置刚体角速度
state.set_angular_velocity(signf(collision_normal.x) * 33.0),让敌人被子弹击中后旋转着倒地——这是 RigidBody2D 独有的物理反馈; - 将
physics_material_override.friction设为 1 增大摩擦帮助停稳; - 调用
collider.disable()让子弹"熄火"并播放命中音效。
子弹本身也是RigidBody2D(player/bullet.gd),发射时赋予初速度Vector2(400.0 * speed_scale, -40)(向右 400、略向上,speed_scale根据朝向取 ±1),并在_shot_bullet()中通过add_collision_exception_with(bullet)让子弹与玩家互不碰撞(player/player.gd)。子弹带有Timer与disable()逻辑,被命中后播放 "shutdown" 动画并标记disabled = true,避免重复触发。
收集物与物理交互:金币、跷跷板、单向平台
金币:Area2D 检测刚体玩家
金币使用Area2D(coin/coin.gd)而非刚体,通过body_entered信号检测:
func _on_body_enter(body: Node2D) -> void: if not taken and body is Player: ($AnimationPlayer as AnimationPlayer).play(&"taken")taken标志防止重复触发,动画 "taken" 播放后金币消失。关卡中摆放了 40+ 个金币实例(见 stage.tscn 的Coins节点),沿平台与跳跃路线分布,作为收集目标。
跷跷板:PinJoint2D 的真实物理反馈
这是 README 特别点名的亮点交互。跷跷板场景 background/seesaw.tscn 由三部分组成:
- Plank(木板):
RigidBody2D,mass = 5.10204,带矩形碰撞体; - Pillar(支柱):
StaticBody2D,固定在地面; - PinJoint2D(销钉关节):
node_a指向木板、node_b指向支柱,把两者铰接成可转动的杠杆。
当玩家跳到木板一端,刚体物理自动产生力矩让另一端翘起——完全无需手写任何跷跷板逻辑,这正是"刚体角色与物理完美交互"的直观体现。同理,玩家也可以跳到敌人头上骑乘,敌人移动时会把玩家一起带走。
单向平台:one_way_collision
platform/one_way_platform.tscn 中CollisionShape2D开启one_way_collision = true,实现经典的"只能从下往上穿过、落到上面站住"的单向平台效果,用于设计从下方跳入的路线。
TileMap 关卡与物理层
地面由TileMapLayer(Godot 4 中TileMap的图层节点)铺成,tileset 定义在 tileset.tres。其中每个图块的physics_layer_0/polygon_0/points都显式给出了碰撞多边形(如(-16, -12, 16, -12, 16, 16, -16, 16)这类 32×32 内的矩形区域,见 tileset.tres),并通过flip_h/flip_v/transpose组合出全方向图块(上下左右与角块),保证铺出的地面处处有真实碰撞。
如果想修改基础图块,关卡内提示文字建议打开 tileset_edit.tscn 在 TileSet 编辑器中调整,再回到 stage.tscn 查看效果。
项目物理配置:为刚体角色调优的全局参数
project.godot 中的物理设置是整套刚体方案能否"跑顺"的前提:
[physics] common/physics_ticks_per_second=120 2d/default_gravity=900 common/physics_interpolation=truephysics_ticks_per_second = 120:物理步长提升到 120Hz(默认 60),使快速移动的刚体角色碰撞检测更精确、运动更平滑,代价是 CPU 开销更高——这正是_integrate_forces中step对应 1/120 秒的原因;2d/default_gravity = 900:2D 重力加速度调得比默认值大,配合JUMP_VELOCITY = 380得到干脆利落的跳跃手感(跳跃高度 ≈ v²/(2g) ≈ 80px);common/physics_interpolation = true:开启物理插值,让 120Hz 的物理结果在低刷新率屏幕上渲染更平滑,是刚体角色画面流畅性的关键开关。
输入映射同样在 project.godot 定义:jump(W / 空格?实际为物理键 W + 方向键上 + 手柄 A)、move_left(A + 左方向键 + 手柄)、move_right(D + 右方向键 + 手柄)、shoot(空格 + 鼠标左键 + 手柄)、spawn(R + 手柄 Y,可在头顶生成一个敌人供测试)。此外 player.tscn 还提供了 4 个TouchScreenButton(Left/Right/Jump/Fire)映射到同名 action,支持触屏移动端操作,并跟随visibility_mode自动显示。
运行与体验
在 Godot 4.x(项目config/features标注 4.7,project.godot)中导入2d/physics_platformer目录,直接运行主场景 stage.tscn 即可:
- 移动:A/D 或方向键左右;跳跃:W / 上方向键(按住跳更高);射击:空格或鼠标左键;生成敌人:R 键在头顶生成一只敌人,方便随时测试骑乘与击杀。
- 观察点:跳到移动平台上随其升降;踩跷跷板一端看另一端翘起;跳到敌人头上骑着它巡逻;开枪击杀敌人看其旋转倒地动画。
- 关卡自带滚动视差背景(background/parallax_bg.tscn 使用多层云、前景贴图滚动),背景音乐为循环播放的
audio/music.ogg。
音乐资源
按 README 说明,背景音乐 "Pompy" 由 Hubert Lamontagne(madbr)创作,音效文件(跳跃、射击、命中、爆炸、金币)均位于 2d/physics_platformer/audio 目录,通过AudioStreamPlayer/AudioStreamPlayer2D挂载到对应场景节点上播放,可在编辑器中替换为自有资源。
参考资料
- 演示 README:2d/physics_platformer/README.md
- 玩家控制器:2d/physics_platformer/player/player.gd、2d/physics_platformer/player/player.tscn
- 敌人 AI:2d/physics_platformer/enemy/enemy.gd
- 子弹:2d/physics_platformer/player/bullet.gd
- 关卡组装与物理道具:2d/physics_platformer/stage.tscn、2d/physics_platformer/background/seesaw.tscn、2d/physics_platformer/platform/one_way_platform.tscn、2d/physics_platformer/platform/moving_platform.tscn
- 项目与瓦片配置:2d/physics_platformer/project.godot、2d/physics_platformer/tileset.tres
如果你希望角色拥有"脚踏实地的精确操控感",可以继续对比仓库中同样位于 2d 分类下使用CharacterBody2D的 kinematic_character 与 platformer 演示,两者对照即可直观理解两种角色控制器在手感与物理交互上的取舍。
- 示例工程
【免费下载链接】godot-demo-projects
Demonstration and Template Projects
相关推荐
Godot 4.7 3D 平台跳跃示例深度解析:基于 CharacterBody3D 构建 Platformer 3D 完整项目
Godot 4.7 3D 平台跳跃示例深度解析:基于 CharacterBody3D 构建 Platformer 3D 完整项目 这篇技术指南以本仓库 3d/p
示例工程Godot-demo-projects中的着色器示例:2D与3D特效实现教程
Godot demo projects中的着色器示例:2D与3D特效实现教程 引言:告别特效实现困境 你是否还在为Godot引擎中着色器 Shader 的复杂语
示例工程GraphiQL 本地测试环境搭建指南:从 test 目录到 Cypress 端到端验证
GraphiQL 本地测试环境搭建指南:从 test 目录到 Cypress 端到端验证 GraphiQL 是浏览器端交互式 GraphQL IDE,在持续迭代
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考