1. 项目概述从零开始在Godot中创建你的第一个可动角色如果你刚打开Godot面对一个空荡荡的3D视口和一堆陌生的节点列表感到无从下手那么你来对地方了。今天我们就来手把手地完成一个游戏开发中最基础、也最激动人心的环节创建你的第一个游戏角色。这不仅仅是拖拽一个模型那么简单而是从场景搭建、节点组织、脚本控制到动画赋予生命的完整流程。无论你是想做一个在平台上跳跃的方块人还是一个在奇幻世界里冒险的英雄第一步都是让这个“角色”在你的世界里动起来。这个过程就是理解Godot核心设计哲学——“场景即节点树”的最佳实践。我们将创建一个经典的第三人称角色控制器它能够响应键盘输入进行移动并在移动时播放对应的动画。这个角色将具备基础的物理碰撞、重力响应以及平滑的转向。我会用大量动图来展示每一步的操作确保即使你是第一次接触Godot也能清晰无误地跟上。最终你将得到一个可以放入任何3D场景中、完全由你控制的角色原型这是你构建任何类型3D游戏从RPG到动作冒险的基石。2. 核心思路与项目结构设计在Godot里做角色切忌一上来就想着写复杂的AI或炫酷的技能。我们的首要目标是建立一个稳固、可扩展的基础框架。这个框架的核心思想是将逻辑、表现和物理分离。2.1 为什么选择“CharacterBody3D CollisionShape3D MeshInstance3D”的组合打开Godot新建一个3D场景。你可能会疑惑根节点该选什么Node3D、RigidBody3D还是CharacterBody3D对于玩家角色CharacterBody3D是官方推荐且最合适的选择。我来解释一下为什么RigidBody3D刚体完全由物理引擎驱动。你给它一个力它就会运动、旋转、碰撞、反弹。它模拟现实世界的物理但正因如此控制起来非常“滑”不适合需要精确响应玩家输入的角色比如马里奥。你很难让它立刻停下或做出精准的跳跃。KinematicBody3D在Godot 3.x中/CharacterBody3D在Godot 4.x中这是为角色控制量身定做的节点。它不受物理引擎的力直接驱动而是由你的代码通过move_and_slide()或move_and_collide()方法来决定如何移动。物理引擎只负责告诉它“你撞到了什么”而移动的决策权完全在你手里。这让你能实现“碰到墙壁就停下”、“沿着斜坡行走”、“跳跃到特定高度”等游戏逻辑而不是物理模拟。所以我们的角色骨架应该是这样的根节点CharacterBody3D。命名为Player。它负责处理移动逻辑和碰撞检测。子节点CollisionShape3D。挂载在Player下。它定义了角色的物理碰撞体积。没有它你的角色就是一个“幽灵”会穿墙而过。我们通常用一个CapsuleShape3D胶囊体因为它结合了圆柱体的稳定和球体的平滑非常适合人形角色不容易在斜坡或角落卡住。视觉表现节点MeshInstance3D。同样挂载在Player下。它用来显示角色的模型。初期我们可以用一个简单的CapsuleMesh胶囊网格或BoxMesh方块来代替重点是先让逻辑跑通。摄像机支架节点Node3D通常命名为Pivot或CameraPivot。挂载在Player下。这是一个空节点作为摄像机的父级。将摄像机放在这个节点下而不是直接放在Player下可以让你独立控制摄像机的旋转比如看向角色背后而不影响角色的移动方向。这个结构清晰地将物理(CollisionShape3D)、逻辑(CharacterBody3D脚本)、视觉(MeshInstance3D)、视角(Camera3D及其父节点)分离开是Godot中构建角色的标准范式。2.2 输入映射让角色听懂你的指令在写代码之前我们需要先告诉Godot“当玩家按下W键时意味着‘向前移动’”。这就是输入映射Input Map。进入项目 - 项目设置 - 输入映射。在这里我们定义“动作Action”。对于基础移动我建议创建四个动作move_forward 绑定W和向上箭头。move_backward绑定S和向下箭头。move_left 绑定A和向左箭头。move_right 绑定D和向右箭头。jump 绑定空格键。为什么用“动作”而不是直接检测按键这带来了巨大的灵活性。未来如果你想支持手柄只需要为move_forward这个动作再添加一个“手柄左摇杆上”的绑定即可代码完全不用改。这是实现多平台输入支持的关键。3. 编写角色控制脚本让角色动起来现在选中我们的PlayerCharacterBody3D节点点击检查器顶部的“添加脚本”按钮。语言选择GDScriptGodot的原生脚本语言语法类似Python与引擎集成度最高。我们将一步步构建这个脚本。3.1 定义基础属性与物理过程首先我们定义一些可调节的参数这样可以在检查器中方便地调整角色手感而无需修改代码。extends CharacterBody3D # 角色移动速度米/秒 export var speed: float 5.0 # 跳跃的初始垂直速度米/秒 export var jump_velocity: float 4.5 # 重力加速度米/秒²。Godot默认是9.8但游戏里通常会调高以获得更快的下落手感。 export var gravity: float 15.0 # 获取摄像机的引用假设摄像机是当前节点的子节点路径为“Pivot/Camera3D” onready var camera $Pivot/Camera3Dexport关键字使得这些变量在编辑器的检查器中可见并可编辑非常适合调试和平衡游戏参数。onready表示这个变量会在节点及其子节点都进入场景树_ready()之后后才被赋值确保我们能正确找到子节点。核心逻辑写在_physics_process(delta)函数中。这个函数在每个物理帧默认每秒60次被调用delta是距离上一帧的时间。所有与物理和移动相关的代码都应该放在这里以保证运动的平滑和稳定。func _physics_process(delta): # 1. 处理重力 if not is_on_floor(): velocity.y - gravity * delta # 2. 处理跳跃输入必须在地面上才能跳 if Input.is_action_just_pressed(jump) and is_on_floor(): velocity.y jump_velocity # 3. 获取移动输入方向 var input_dir Input.get_vector(move_left, move_right, move_forward, move_backward) # 4. 将2D输入方向转换为3D世界空间的方向向量 var direction (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() # 5. 应用水平移动速度 if direction: velocity.x direction.x * speed velocity.z direction.z * speed # 可选让角色面朝移动方向 # $Pivot.look_at(global_position direction, Vector3.UP) else: # 没有输入时逐渐停止水平移动模拟摩擦力 velocity.x move_toward(velocity.x, 0, speed) velocity.z move_toward(velocity.z, 0, speed) # 6. 执行移动这是CharacterBody3D的核心方法。 move_and_slide()关键点解析is_on_floor():CharacterBody3D提供的方法用于检测角色当前是否站在地面上。这是实现可靠跳跃逻辑的基础。Input.get_vector(): 一个非常方便的函数它根据四个动作左、右、前、后返回一个归一化的2D向量。如果同时按住W和D它会返回一个指向右前方的向量(0.707, 0.707)。transform.basis * Vector3(...): 这是将局部空间的方向转换到世界空间的关键。transform.basis代表了节点当前的旋转矩阵。假设你按下了“W”向前这个“前”最初是角色的局部Z轴负方向。通过乘以旋转矩阵我们得到了在世界坐标系中“角色面前”的真正方向。这样无论角色面朝哪里按下W它都会朝其面对的方向前进。move_and_slide(): 这是CharacterBody3D的灵魂。它根据velocity向量移动角色并自动处理与场景中其他PhysicsBody如StaticBody3D墙壁、RigidBody3D箱子的碰撞。碰撞后它会自动调整velocity例如撞墙后水平速度归零并更新is_on_floor()、is_on_wall()等状态。3.2 完善移动与摄像机控制上面的代码已经能让角色移动和跳跃了但有两个地方可以优化空中控制通常角色在空中时水平移动控制会减弱或保持。摄像机相对移动更常见的需求是角色的“前、后、左、右”是相对于摄像机视角的而不是角色自身的朝向除非是坦克式控制。为了实现摄像机相对移动我们需要修改方向计算的部分func _physics_process(delta): # ... 重力与跳跃代码保持不变 ... # 获取移动输入 var input_dir Input.get_vector(move_left, move_right, move_forward, move_backward) # 计算相对于摄像机的方向 var direction Vector3.ZERO if camera: # 获取摄像机的全局变换但忽略其俯仰pitch旋转只使用水平yaw旋转。 var camera_basis camera.global_transform.basis # 创建一个只包含水平旋转的基 var flat_basis Basis(camera_basis.get_rotation_quaternion() * Vector3.FORWARD, Vector3.UP) # 将输入向量转换到这个“扁平”的摄像机空间 direction (flat_basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() # 应用速度区分地面和空中 if is_on_floor(): if direction: velocity.x direction.x * speed velocity.z direction.z * speed else: # 地面摩擦力 velocity.x move_toward(velocity.x, 0, speed * 2 * delta) # 更快的停止 velocity.z move_toward(velocity.z, 0, speed * 2 * delta) else: # 空中控制通常允许少量空气转向但惯性更大 if direction: var air_control 0.1 # 空中控制系数0-1之间 velocity.x lerp(velocity.x, direction.x * speed, air_control * delta * 10) velocity.z lerp(velocity.z, direction.z * speed, air_control * delta * 10) # 空中保持原有水平速度仅受重力影响 move_and_slide()lerp函数用于线性插值在这里实现了平滑的空中转向效果。air_control系数越小空中改变方向越困难。4. 添加视觉模型与动画一个只会滑动的胶囊体显然不够。我们需要为它赋予形象和生命。4.1 替换基础网格在场景树中选中那个临时的MeshInstance3D胶囊体。在检查器中点击Mesh属性选择New CapsuleMesh或New BoxMesh。你可以调整它的尺寸使其匹配CollisionShape3D的大小。更好的方法是从文件系统面板拖拽一个准备好的3D模型文件如.glb, .gltf, .fbx到MeshInstance3D的Mesh属性上或者直接拖到场景树中作为MeshInstance3D的子节点。重要步骤调整模型原点。导入的模型其轴心点原点可能在脚底也可能在中心。对于角色控制器我们希望碰撞体的底部在地面模型的脚也在地面。你需要选中你的模型网格节点。在3D视口中使用移动工具快捷键W将其沿Y轴向上移动直到模型的脚部与CollisionShape3D胶囊体的底部大致对齐。更专业的做法是在3D建模软件中就将模型的原点设置在脚底然后重新导出。4.2 使用AnimationPlayer添加动画这是让角色“活过来”的关键。为Player节点添加一个AnimationPlayer子节点。创建Idle待机动画在底部打开Animation面板。点击Animation下拉菜单 -New命名为idle。确保循环播放按钮一个箭头形成环的图标是按下的。我们希望角色有一个轻微的呼吸起伏。将时间线拖到0秒。在场景树中选中你的视觉模型节点例如一个MeshInstance3D或一个骨架根节点。在检查器中找到Transform - Position.y点击旁边的小钥匙图标添加一个关键帧。将时间线拖到1秒将Position.y稍微调高一点比如0.05米再次点击钥匙图标添加关键帧。将时间线拖到2秒将Position.y调回原值添加关键帧。点击播放按钮预览你会看到模型在轻微地上下浮动。创建Run奔跑动画同样点击New创建名为run的动画。如果你有自己的奔跑动画FBX文件可以直接导入在Animation面板点击Animation-Import选择你的FBX文件然后从列表中选择奔跑动画剪辑。如果你没有现成动画可以用类似方法制作一个简单的循环让模型在奔跑时身体前倾手臂摆动如果是人形。这需要你有一个带骨架的模型并对骨骼做关键帧动画。Godot的动画编辑器支持直接对骨骼旋转制作关键帧。通过代码控制动画播放现在我们需要在脚本中根据角色状态是否在移动、是否在地面来切换动画。首先获取AnimationPlayer的引用onready var animation_player $AnimationPlayer然后在_physics_process函数的末尾根据状态决定播放哪个动画func _physics_process(delta): # ... 之前的移动和物理计算代码 ... move_and_slide() # 动画状态逻辑 if is_on_floor(): if velocity.length() 0.1: # 如果速度大于一个很小的阈值认为在移动 animation_player.play(run) # 可以同步调整动画播放速度使其匹配移动速度 # animation_player.speed_scale velocity.length() / speed else: animation_player.play(idle) else: if velocity.y 0: animation_player.play(jump) # 假设你有一个跳跃动画 else: animation_player.play(fall) # 假设你有一个下落动画注意事项animation_player.play()会立即切换到指定动画。如果你希望动画平滑过渡需要使用AnimationTree节点和状态机这是更高级的动画控制方式。确保你的动画名称与play()函数中的字符串完全一致包括大小写。5. 构建测试环境与常见问题排查角色做好了需要在一个环境中测试。创建一个简单的测试关卡新建一个StaticBody3D节点作为地面为其添加一个CollisionShape3D形状选择BoxShape3D并拉大尺寸如50, 1, 50。添加一个MeshInstance3D作为地面的视觉表现可以使用BoxMesh。再创建几个StaticBody3D盒子作为墙壁或障碍物。确保你的Player场景已经保存例如为player.tscn然后将其实例化到主场景中。只需从文件系统面板将player.tscn拖入3D视口即可。运行游戏F5用WASD和空格键控制你的角色吧5.1 常见问题与解决方案实录在实际操作中你几乎一定会遇到下面这些问题。别担心这都是学习过程中的必经之路。问题现象可能原因解决方案角色下坠穿过地面1. 重力值(gravity)过大单帧位移超过了地面厚度。2. 地面CollisionShape3D的Layer和角色的Mask没有对应。1. 减小gravity或增加物理帧率在项目设置中调整Physics - Common - Physics Fps但更推荐增加地面碰撞体的厚度。2. 检查地面和角色的Collision属性。确保地面的Collision Layer至少有一层被勾选如第1层并且角色的Collision Mask也勾选了对应的层如也勾选第1层。角色移动时“抖动”或“卡进”地面CollisionShape3D的形状如胶囊体底部是球面与地面平面接触点计算可能不稳定。1. 这是move_and_slide()在陡峭斜坡或高速下的已知问题。可以尝试使用move_and_slide_with_snap()Godot 3.x或确保在Godot 4.x中正确使用floor_stop_on_slope等参数。2. 稍微增加一点floor_max_angle默认是45度允许角色在更陡的坡上停留。3. 确保gravity * delta的值不要太大。按下按键角色不动1. 输入映射Input Map中的动作名称与代码中Input.is_action_pressed使用的字符串不匹配。2. 速度计算错误direction向量为(0,0,0)。1. 仔细核对项目设置中的动作名和代码中的动作名必须完全一致。2. 在_physics_process中添加print(direction)或使用编辑器底部的“调试器”面板查看变量值检查方向计算逻辑。角色移动方向奇怪比如按前却往左走摄像机相对移动的方向计算错误。camera.global_transform.basis提取不正确或向量乘法顺序有误。简化测试先将direction直接设为Vector3(input_dir.x, 0, input_dir.y)看是否是基础的“局部空间”移动。如果是再逐步调试摄像机转换部分。确保camera变量正确获取到了节点。跳跃不灵敏或连续跳1.is_on_floor()检测不准。2. 跳跃输入检测放在了错误的位置。1.is_on_floor()在调用move_and_slide()之后才被更新。确保你的跳跃检测代码在move_and_slide()之前但重力应用之后。2. 使用Input.is_action_just_pressed(“jump”)而不是is_action_pressed确保只触发一次。动画不播放或闪烁1. 动画名称拼写错误。2. 动画没有设置为循环Loop播放一次后就停止了。3. 动画状态逻辑判断条件有重叠或漏洞。1. 再次检查play()里的字符串。2. 在AnimationPlayer中选中动画在检查器中勾选Loop。3. 使用更明确的状态机。例如引入一个state变量如IDLE,RUNNING,JUMPING,FALLING在_physics_process中根据物理状态更新state然后根据state播放对应动画。5.2 性能与优化小贴士碰撞形状对于复杂角色模型不要使用其高精度网格作为碰撞体。始终使用简单的几何体胶囊、立方体、球体或它们的组合来近似。你可以在CollisionShape3D下添加多个CollisionShape3D节点来组成一个复合碰撞体。LOD细节层次如果角色模型面数很高当它距离摄像机很远时可以使用一个低面数的简化模型来替换以提升渲染性能。Godot有自动生成LOD的工具也可以在建模软件中手动制作。动画树AnimationTree当角色动画变得复杂待机、走、跑、跳、攻击等时AnimationPlayer的直接切换会显得生硬。学习使用AnimationTree节点和AnimationNodeStateMachine它可以管理动画状态、混合过渡是实现专业手感动画系统的核心。6. 从原型到可用的角色控制器至此你已经拥有了一个功能完整的3D角色控制器原型。但它还是一个“原型”。要把它变成一个健壮的、可用于实际项目的角色控制器你还需要考虑以下扩展点状态机用枚举enum或字符串明确管理角色状态IDLE,WALK,RUN,JUMP,FALL,ATTACK等。所有逻辑移动、动画、音效都基于当前状态。摄像机碰撞让摄像机在碰到墙壁时自动拉近避免穿墙。这可以通过从角色向摄像机发射射线RayCast3D来实现。斜坡与楼梯处理更精细地处理move_and_slide()的参数如floor_max_angle,floor_stop_on_slope或者使用RayCast3D检测前方是否有台阶并施加一个向上的力。双段跳、蹬墙跳在状态机中加入跳跃计数和墙壁检测状态实现更丰富的移动机制。动画根运动Root Motion对于复杂的攻击或特殊移动动画让动画本身驱动角色的位移而不是代码。这需要在AnimationPlayer中启用根运动轨道并在脚本中读取。创建第一个角色的过程本质上是在学习Godot如何组织代码与场景、如何处理输入与物理、如何连接逻辑与表现。不要满足于让角色动起来多去调整speed、jump_velocity、gravity这些参数感受它们如何影响操作手感。手感是游戏性的灵魂而这一切都始于你今天搭建的这个基础框架。试着给你的方块人加上一段冲刺动画或者让它在落地时播放一个缓冲翻滚你会发现赋予虚拟角色以生命其乐无穷。
Godot 4 3D角色控制器开发指南:从CharacterBody3D到动画系统
1. 项目概述从零开始在Godot中创建你的第一个可动角色如果你刚打开Godot面对一个空荡荡的3D视口和一堆陌生的节点列表感到无从下手那么你来对地方了。今天我们就来手把手地完成一个游戏开发中最基础、也最激动人心的环节创建你的第一个游戏角色。这不仅仅是拖拽一个模型那么简单而是从场景搭建、节点组织、脚本控制到动画赋予生命的完整流程。无论你是想做一个在平台上跳跃的方块人还是一个在奇幻世界里冒险的英雄第一步都是让这个“角色”在你的世界里动起来。这个过程就是理解Godot核心设计哲学——“场景即节点树”的最佳实践。我们将创建一个经典的第三人称角色控制器它能够响应键盘输入进行移动并在移动时播放对应的动画。这个角色将具备基础的物理碰撞、重力响应以及平滑的转向。我会用大量动图来展示每一步的操作确保即使你是第一次接触Godot也能清晰无误地跟上。最终你将得到一个可以放入任何3D场景中、完全由你控制的角色原型这是你构建任何类型3D游戏从RPG到动作冒险的基石。2. 核心思路与项目结构设计在Godot里做角色切忌一上来就想着写复杂的AI或炫酷的技能。我们的首要目标是建立一个稳固、可扩展的基础框架。这个框架的核心思想是将逻辑、表现和物理分离。2.1 为什么选择“CharacterBody3D CollisionShape3D MeshInstance3D”的组合打开Godot新建一个3D场景。你可能会疑惑根节点该选什么Node3D、RigidBody3D还是CharacterBody3D对于玩家角色CharacterBody3D是官方推荐且最合适的选择。我来解释一下为什么RigidBody3D刚体完全由物理引擎驱动。你给它一个力它就会运动、旋转、碰撞、反弹。它模拟现实世界的物理但正因如此控制起来非常“滑”不适合需要精确响应玩家输入的角色比如马里奥。你很难让它立刻停下或做出精准的跳跃。KinematicBody3D在Godot 3.x中/CharacterBody3D在Godot 4.x中这是为角色控制量身定做的节点。它不受物理引擎的力直接驱动而是由你的代码通过move_and_slide()或move_and_collide()方法来决定如何移动。物理引擎只负责告诉它“你撞到了什么”而移动的决策权完全在你手里。这让你能实现“碰到墙壁就停下”、“沿着斜坡行走”、“跳跃到特定高度”等游戏逻辑而不是物理模拟。所以我们的角色骨架应该是这样的根节点CharacterBody3D。命名为Player。它负责处理移动逻辑和碰撞检测。子节点CollisionShape3D。挂载在Player下。它定义了角色的物理碰撞体积。没有它你的角色就是一个“幽灵”会穿墙而过。我们通常用一个CapsuleShape3D胶囊体因为它结合了圆柱体的稳定和球体的平滑非常适合人形角色不容易在斜坡或角落卡住。视觉表现节点MeshInstance3D。同样挂载在Player下。它用来显示角色的模型。初期我们可以用一个简单的CapsuleMesh胶囊网格或BoxMesh方块来代替重点是先让逻辑跑通。摄像机支架节点Node3D通常命名为Pivot或CameraPivot。挂载在Player下。这是一个空节点作为摄像机的父级。将摄像机放在这个节点下而不是直接放在Player下可以让你独立控制摄像机的旋转比如看向角色背后而不影响角色的移动方向。这个结构清晰地将物理(CollisionShape3D)、逻辑(CharacterBody3D脚本)、视觉(MeshInstance3D)、视角(Camera3D及其父节点)分离开是Godot中构建角色的标准范式。2.2 输入映射让角色听懂你的指令在写代码之前我们需要先告诉Godot“当玩家按下W键时意味着‘向前移动’”。这就是输入映射Input Map。进入项目 - 项目设置 - 输入映射。在这里我们定义“动作Action”。对于基础移动我建议创建四个动作move_forward 绑定W和向上箭头。move_backward绑定S和向下箭头。move_left 绑定A和向左箭头。move_right 绑定D和向右箭头。jump 绑定空格键。为什么用“动作”而不是直接检测按键这带来了巨大的灵活性。未来如果你想支持手柄只需要为move_forward这个动作再添加一个“手柄左摇杆上”的绑定即可代码完全不用改。这是实现多平台输入支持的关键。3. 编写角色控制脚本让角色动起来现在选中我们的PlayerCharacterBody3D节点点击检查器顶部的“添加脚本”按钮。语言选择GDScriptGodot的原生脚本语言语法类似Python与引擎集成度最高。我们将一步步构建这个脚本。3.1 定义基础属性与物理过程首先我们定义一些可调节的参数这样可以在检查器中方便地调整角色手感而无需修改代码。extends CharacterBody3D # 角色移动速度米/秒 export var speed: float 5.0 # 跳跃的初始垂直速度米/秒 export var jump_velocity: float 4.5 # 重力加速度米/秒²。Godot默认是9.8但游戏里通常会调高以获得更快的下落手感。 export var gravity: float 15.0 # 获取摄像机的引用假设摄像机是当前节点的子节点路径为“Pivot/Camera3D” onready var camera $Pivot/Camera3Dexport关键字使得这些变量在编辑器的检查器中可见并可编辑非常适合调试和平衡游戏参数。onready表示这个变量会在节点及其子节点都进入场景树_ready()之后后才被赋值确保我们能正确找到子节点。核心逻辑写在_physics_process(delta)函数中。这个函数在每个物理帧默认每秒60次被调用delta是距离上一帧的时间。所有与物理和移动相关的代码都应该放在这里以保证运动的平滑和稳定。func _physics_process(delta): # 1. 处理重力 if not is_on_floor(): velocity.y - gravity * delta # 2. 处理跳跃输入必须在地面上才能跳 if Input.is_action_just_pressed(jump) and is_on_floor(): velocity.y jump_velocity # 3. 获取移动输入方向 var input_dir Input.get_vector(move_left, move_right, move_forward, move_backward) # 4. 将2D输入方向转换为3D世界空间的方向向量 var direction (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() # 5. 应用水平移动速度 if direction: velocity.x direction.x * speed velocity.z direction.z * speed # 可选让角色面朝移动方向 # $Pivot.look_at(global_position direction, Vector3.UP) else: # 没有输入时逐渐停止水平移动模拟摩擦力 velocity.x move_toward(velocity.x, 0, speed) velocity.z move_toward(velocity.z, 0, speed) # 6. 执行移动这是CharacterBody3D的核心方法。 move_and_slide()关键点解析is_on_floor():CharacterBody3D提供的方法用于检测角色当前是否站在地面上。这是实现可靠跳跃逻辑的基础。Input.get_vector(): 一个非常方便的函数它根据四个动作左、右、前、后返回一个归一化的2D向量。如果同时按住W和D它会返回一个指向右前方的向量(0.707, 0.707)。transform.basis * Vector3(...): 这是将局部空间的方向转换到世界空间的关键。transform.basis代表了节点当前的旋转矩阵。假设你按下了“W”向前这个“前”最初是角色的局部Z轴负方向。通过乘以旋转矩阵我们得到了在世界坐标系中“角色面前”的真正方向。这样无论角色面朝哪里按下W它都会朝其面对的方向前进。move_and_slide(): 这是CharacterBody3D的灵魂。它根据velocity向量移动角色并自动处理与场景中其他PhysicsBody如StaticBody3D墙壁、RigidBody3D箱子的碰撞。碰撞后它会自动调整velocity例如撞墙后水平速度归零并更新is_on_floor()、is_on_wall()等状态。3.2 完善移动与摄像机控制上面的代码已经能让角色移动和跳跃了但有两个地方可以优化空中控制通常角色在空中时水平移动控制会减弱或保持。摄像机相对移动更常见的需求是角色的“前、后、左、右”是相对于摄像机视角的而不是角色自身的朝向除非是坦克式控制。为了实现摄像机相对移动我们需要修改方向计算的部分func _physics_process(delta): # ... 重力与跳跃代码保持不变 ... # 获取移动输入 var input_dir Input.get_vector(move_left, move_right, move_forward, move_backward) # 计算相对于摄像机的方向 var direction Vector3.ZERO if camera: # 获取摄像机的全局变换但忽略其俯仰pitch旋转只使用水平yaw旋转。 var camera_basis camera.global_transform.basis # 创建一个只包含水平旋转的基 var flat_basis Basis(camera_basis.get_rotation_quaternion() * Vector3.FORWARD, Vector3.UP) # 将输入向量转换到这个“扁平”的摄像机空间 direction (flat_basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() # 应用速度区分地面和空中 if is_on_floor(): if direction: velocity.x direction.x * speed velocity.z direction.z * speed else: # 地面摩擦力 velocity.x move_toward(velocity.x, 0, speed * 2 * delta) # 更快的停止 velocity.z move_toward(velocity.z, 0, speed * 2 * delta) else: # 空中控制通常允许少量空气转向但惯性更大 if direction: var air_control 0.1 # 空中控制系数0-1之间 velocity.x lerp(velocity.x, direction.x * speed, air_control * delta * 10) velocity.z lerp(velocity.z, direction.z * speed, air_control * delta * 10) # 空中保持原有水平速度仅受重力影响 move_and_slide()lerp函数用于线性插值在这里实现了平滑的空中转向效果。air_control系数越小空中改变方向越困难。4. 添加视觉模型与动画一个只会滑动的胶囊体显然不够。我们需要为它赋予形象和生命。4.1 替换基础网格在场景树中选中那个临时的MeshInstance3D胶囊体。在检查器中点击Mesh属性选择New CapsuleMesh或New BoxMesh。你可以调整它的尺寸使其匹配CollisionShape3D的大小。更好的方法是从文件系统面板拖拽一个准备好的3D模型文件如.glb, .gltf, .fbx到MeshInstance3D的Mesh属性上或者直接拖到场景树中作为MeshInstance3D的子节点。重要步骤调整模型原点。导入的模型其轴心点原点可能在脚底也可能在中心。对于角色控制器我们希望碰撞体的底部在地面模型的脚也在地面。你需要选中你的模型网格节点。在3D视口中使用移动工具快捷键W将其沿Y轴向上移动直到模型的脚部与CollisionShape3D胶囊体的底部大致对齐。更专业的做法是在3D建模软件中就将模型的原点设置在脚底然后重新导出。4.2 使用AnimationPlayer添加动画这是让角色“活过来”的关键。为Player节点添加一个AnimationPlayer子节点。创建Idle待机动画在底部打开Animation面板。点击Animation下拉菜单 -New命名为idle。确保循环播放按钮一个箭头形成环的图标是按下的。我们希望角色有一个轻微的呼吸起伏。将时间线拖到0秒。在场景树中选中你的视觉模型节点例如一个MeshInstance3D或一个骨架根节点。在检查器中找到Transform - Position.y点击旁边的小钥匙图标添加一个关键帧。将时间线拖到1秒将Position.y稍微调高一点比如0.05米再次点击钥匙图标添加关键帧。将时间线拖到2秒将Position.y调回原值添加关键帧。点击播放按钮预览你会看到模型在轻微地上下浮动。创建Run奔跑动画同样点击New创建名为run的动画。如果你有自己的奔跑动画FBX文件可以直接导入在Animation面板点击Animation-Import选择你的FBX文件然后从列表中选择奔跑动画剪辑。如果你没有现成动画可以用类似方法制作一个简单的循环让模型在奔跑时身体前倾手臂摆动如果是人形。这需要你有一个带骨架的模型并对骨骼做关键帧动画。Godot的动画编辑器支持直接对骨骼旋转制作关键帧。通过代码控制动画播放现在我们需要在脚本中根据角色状态是否在移动、是否在地面来切换动画。首先获取AnimationPlayer的引用onready var animation_player $AnimationPlayer然后在_physics_process函数的末尾根据状态决定播放哪个动画func _physics_process(delta): # ... 之前的移动和物理计算代码 ... move_and_slide() # 动画状态逻辑 if is_on_floor(): if velocity.length() 0.1: # 如果速度大于一个很小的阈值认为在移动 animation_player.play(run) # 可以同步调整动画播放速度使其匹配移动速度 # animation_player.speed_scale velocity.length() / speed else: animation_player.play(idle) else: if velocity.y 0: animation_player.play(jump) # 假设你有一个跳跃动画 else: animation_player.play(fall) # 假设你有一个下落动画注意事项animation_player.play()会立即切换到指定动画。如果你希望动画平滑过渡需要使用AnimationTree节点和状态机这是更高级的动画控制方式。确保你的动画名称与play()函数中的字符串完全一致包括大小写。5. 构建测试环境与常见问题排查角色做好了需要在一个环境中测试。创建一个简单的测试关卡新建一个StaticBody3D节点作为地面为其添加一个CollisionShape3D形状选择BoxShape3D并拉大尺寸如50, 1, 50。添加一个MeshInstance3D作为地面的视觉表现可以使用BoxMesh。再创建几个StaticBody3D盒子作为墙壁或障碍物。确保你的Player场景已经保存例如为player.tscn然后将其实例化到主场景中。只需从文件系统面板将player.tscn拖入3D视口即可。运行游戏F5用WASD和空格键控制你的角色吧5.1 常见问题与解决方案实录在实际操作中你几乎一定会遇到下面这些问题。别担心这都是学习过程中的必经之路。问题现象可能原因解决方案角色下坠穿过地面1. 重力值(gravity)过大单帧位移超过了地面厚度。2. 地面CollisionShape3D的Layer和角色的Mask没有对应。1. 减小gravity或增加物理帧率在项目设置中调整Physics - Common - Physics Fps但更推荐增加地面碰撞体的厚度。2. 检查地面和角色的Collision属性。确保地面的Collision Layer至少有一层被勾选如第1层并且角色的Collision Mask也勾选了对应的层如也勾选第1层。角色移动时“抖动”或“卡进”地面CollisionShape3D的形状如胶囊体底部是球面与地面平面接触点计算可能不稳定。1. 这是move_and_slide()在陡峭斜坡或高速下的已知问题。可以尝试使用move_and_slide_with_snap()Godot 3.x或确保在Godot 4.x中正确使用floor_stop_on_slope等参数。2. 稍微增加一点floor_max_angle默认是45度允许角色在更陡的坡上停留。3. 确保gravity * delta的值不要太大。按下按键角色不动1. 输入映射Input Map中的动作名称与代码中Input.is_action_pressed使用的字符串不匹配。2. 速度计算错误direction向量为(0,0,0)。1. 仔细核对项目设置中的动作名和代码中的动作名必须完全一致。2. 在_physics_process中添加print(direction)或使用编辑器底部的“调试器”面板查看变量值检查方向计算逻辑。角色移动方向奇怪比如按前却往左走摄像机相对移动的方向计算错误。camera.global_transform.basis提取不正确或向量乘法顺序有误。简化测试先将direction直接设为Vector3(input_dir.x, 0, input_dir.y)看是否是基础的“局部空间”移动。如果是再逐步调试摄像机转换部分。确保camera变量正确获取到了节点。跳跃不灵敏或连续跳1.is_on_floor()检测不准。2. 跳跃输入检测放在了错误的位置。1.is_on_floor()在调用move_and_slide()之后才被更新。确保你的跳跃检测代码在move_and_slide()之前但重力应用之后。2. 使用Input.is_action_just_pressed(“jump”)而不是is_action_pressed确保只触发一次。动画不播放或闪烁1. 动画名称拼写错误。2. 动画没有设置为循环Loop播放一次后就停止了。3. 动画状态逻辑判断条件有重叠或漏洞。1. 再次检查play()里的字符串。2. 在AnimationPlayer中选中动画在检查器中勾选Loop。3. 使用更明确的状态机。例如引入一个state变量如IDLE,RUNNING,JUMPING,FALLING在_physics_process中根据物理状态更新state然后根据state播放对应动画。5.2 性能与优化小贴士碰撞形状对于复杂角色模型不要使用其高精度网格作为碰撞体。始终使用简单的几何体胶囊、立方体、球体或它们的组合来近似。你可以在CollisionShape3D下添加多个CollisionShape3D节点来组成一个复合碰撞体。LOD细节层次如果角色模型面数很高当它距离摄像机很远时可以使用一个低面数的简化模型来替换以提升渲染性能。Godot有自动生成LOD的工具也可以在建模软件中手动制作。动画树AnimationTree当角色动画变得复杂待机、走、跑、跳、攻击等时AnimationPlayer的直接切换会显得生硬。学习使用AnimationTree节点和AnimationNodeStateMachine它可以管理动画状态、混合过渡是实现专业手感动画系统的核心。6. 从原型到可用的角色控制器至此你已经拥有了一个功能完整的3D角色控制器原型。但它还是一个“原型”。要把它变成一个健壮的、可用于实际项目的角色控制器你还需要考虑以下扩展点状态机用枚举enum或字符串明确管理角色状态IDLE,WALK,RUN,JUMP,FALL,ATTACK等。所有逻辑移动、动画、音效都基于当前状态。摄像机碰撞让摄像机在碰到墙壁时自动拉近避免穿墙。这可以通过从角色向摄像机发射射线RayCast3D来实现。斜坡与楼梯处理更精细地处理move_and_slide()的参数如floor_max_angle,floor_stop_on_slope或者使用RayCast3D检测前方是否有台阶并施加一个向上的力。双段跳、蹬墙跳在状态机中加入跳跃计数和墙壁检测状态实现更丰富的移动机制。动画根运动Root Motion对于复杂的攻击或特殊移动动画让动画本身驱动角色的位移而不是代码。这需要在AnimationPlayer中启用根运动轨道并在脚本中读取。创建第一个角色的过程本质上是在学习Godot如何组织代码与场景、如何处理输入与物理、如何连接逻辑与表现。不要满足于让角色动起来多去调整speed、jump_velocity、gravity这些参数感受它们如何影响操作手感。手感是游戏性的灵魂而这一切都始于你今天搭建的这个基础框架。试着给你的方块人加上一段冲刺动画或者让它在落地时播放一个缓冲翻滚你会发现赋予虚拟角色以生命其乐无穷。