Godot 3D角色移动控制:从CharacterBody3D到物理帧的完整实现
1. 项目概述:从零开始的 Godot 之旅 — EP5:控制角色移动
如果你刚刚接触 Godot,并且已经跟着教程走完了创建场景、添加节点、设置碰撞这些基础步骤,那么恭喜你,你已经搭建好了一个角色的“躯壳”。现在,是时候赋予这个躯壳“灵魂”了——也就是让它在你的游戏世界里动起来。控制角色移动,这几乎是所有游戏开发旅程中第一个真正让人兴奋的里程碑。它不仅仅是让一个精灵在屏幕上滑动,更是你与游戏世界建立交互的开始。在 Godot 中,这个过程既直观又强大,尤其是当你开始理解CharacterBody3D节点和_physics_process函数背后的逻辑时。
这篇文章,就是为你准备的“角色移动”实操指南。我们将不依赖任何预设的脚本模板,从零开始,一行一行地构建一个在 3D 空间中响应键盘输入、平滑移动并受重力影响的角色控制器。无论你是想制作一个跑酷游戏、一个 RPG,还是一个简单的平台跳跃游戏,这里面的核心逻辑都是相通的。我会带你深入理解velocity(速度向量)、move_and_slide()方法,以及如何处理输入和重力,让你不仅知道怎么做,更明白为什么这么做。准备好了吗?让我们开始这段让角色“活”起来的旅程。
2. 核心思路与架构设计:为什么是 CharacterBody3D 和物理帧?
在开始敲代码之前,我们先花点时间理清思路。Godot 提供了多种处理移动和碰撞的方式,比如RigidBody3D(刚体,物理模拟驱动)和Area3D(区域,用于检测但不处理碰撞)。但对于一个需要玩家精确控制的角色(比如马里奥、塞尔达),CharacterBody3D是官方推荐且最合适的节点类型。
2.1 为什么选择 CharacterBody3D?
CharacterBody3D是一个专门为需要自定义移动逻辑的角色设计的节点。它与物理引擎紧密集成,但把移动控制的“方向盘”完全交给了你。这意味着:
- 完全可控:你可以精确计算每一帧角色应该移动到哪里,而不是像刚体那样被物理规则(如动量、摩擦力)完全支配。
- 内置碰撞响应:它提供了
move_and_collide()和move_and_slide()等方法,能自动处理与场景中其他PhysicsBody的碰撞,并返回碰撞信息,让你可以据此做出反应(比如碰到墙就停止,碰到地板就站立)。 - 地面检测:它内置了
is_on_floor()、is_on_wall()、is_on_ceiling()等方法,这对于实现跳跃、攀爬等平台游戏核心机制至关重要。
简单来说,CharacterBody3D是“物理驱动的可控体”,它为你处理了复杂的碰撞检测和响应,让你能专注于编写“如何移动”的逻辑。
2.2 物理帧 (_physics_process) 与普通帧 (_process) 的区别
这是 Godot 开发中一个关键概念,理解错了可能会导致移动卡顿或不稳定。
_process(delta):每渲染一帧调用一次。调用频率取决于显示器的刷新率(如 60Hz 就是每秒60次)。它适合处理与画面渲染紧密相关、但对时间精度要求不高的逻辑,比如 UI 更新、非物理的动画播放。_physics_process(delta):在固定的时间间隔调用(默认每秒60次,与渲染帧率脱钩)。这个间隔就是delta参数,通常是固定的(如 1/60 秒)。所有与物理、碰撞、移动相关的代码都应该放在这里。
为什么?因为物理引擎(如 Godot 内置的 Godot Physics 或 Jolt)需要在稳定的时间步长下进行模拟,才能保证碰撞检测和物体运动的准确性。如果放在_process里,当游戏帧率波动时,角色的移动速度也会时快时慢,碰撞检测可能出错。_physics_process保证了物理模拟的稳定性。
所以,我们的移动代码将全部写在_physics_process(delta)函数中。
2.3 移动逻辑的蓝图
我们的移动控制器将围绕一个核心向量展开:velocity(速度)。在每一帧的_physics_process中,我们会:
- 读取输入:检查玩家按下了哪些方向键(WASD 或方向键)。
- 计算水平方向速度:根据输入,计算出一个表示水平移动方向和速度的向量。
- 应用重力:如果角色在空中,则根据重力加速度持续修改垂直方向(Y轴)的速度,使其下落。
- 组合最终速度:将水平速度和垂直速度合并到
velocity向量中。 - 执行移动:调用
move_and_slide(),让CharacterBody3D根据velocity移动角色,并自动处理碰撞。 - 更新状态:移动后,根据碰撞结果(如是否着地)更新角色状态,为下一帧计算做准备。
这个流程构成了一个清晰的循环,是绝大多数 3D 角色控制器的基石。
3. 核心细节解析与实操要点
理解了整体框架,我们来拆解每一个环节,看看具体怎么做,以及有哪些需要注意的“坑”。
3.1 输入处理:从按键到方向向量
Godot 的输入系统非常灵活,我们使用Input单例来检测动作(Action)。首先,确保你已经在项目设置的“输入映射”中定义了move_left,move_right,move_forward,move_back这些动作,并关联了相应的按键(如 A/D/W/S)。
在代码中,我们这样获取输入:
var direction = Vector3.ZERO # 初始化一个零向量 if Input.is_action_pressed("move_right"): direction.x += 1 if Input.is_action_pressed("move_left"): direction.x -= 1 if Input.is_action_pressed("move_back"): # 注意:在3D中,向前是-Z,向后是+Z direction.z += 1 if Input.is_action_pressed("move_forward"): direction.z -= 1这里有几个要点:
is_action_pressed:在按键被按住的每一帧都返回true,适合持续移动。- 方向轴:在 Godot 的 3D 坐标系中,默认是Y轴向上,X轴向右,Z轴向内(屏幕深处)。所以
move_forward对应direction.z -= 1,是向 Z 轴负方向移动。 - 向量叠加:如果同时按下
move_right(x+=1) 和move_forward(z-=1),direction会变成(1, 0, -1)。这个向量的长度(模)是 √2 ≈ 1.414。这意味着对角移动会比单轴移动快 1.414 倍,这通常不是我们想要的。
注意:必须对方向向量进行归一化(Normalize)。归一化就是将向量缩放到长度为 1,只保留方向信息。这样,无论朝哪个方向移动,基础速度都是一致的。
if direction != Vector3.ZERO: direction = direction.normalized()3.2 速度向量的构成:水平移动与垂直重力
CharacterBody3D有一个velocity属性,它是一个Vector3。我们的任务就是在每一帧正确计算它。
# 1. 计算水平地面速度 var horizontal_velocity = direction * speed # speed 是你定义的移动速度标量,如 10 # 2. 处理垂直方向的重力 var vertical_velocity = velocity.y # 继承上一帧的垂直速度 if not is_on_floor(): vertical_velocity -= fall_acceleration * delta # fall_acceleration 是重力加速度,如 75 # 注意:这里用减号,因为Y轴向上为正,重力是向下的力。 # 3. 组合速度向量 target_velocity.x = horizontal_velocity.x target_velocity.z = horizontal_velocity.z target_velocity.y = vertical_velocity # 4. 赋值给角色的 velocity velocity = target_velocity关键解释:
delta:这是_physics_process函数的参数,代表自上一帧物理更新以来经过的时间(通常是固定的,如 1/60 秒)。任何基于时间的连续变化(如速度、位移)都必须乘以delta,这样才能保证在不同帧率下行为一致。例如,vertical_velocity -= fall_acceleration * delta意味着“这一帧,垂直速度减少(重力加速度 × 帧时间)”。is_on_floor():这是CharacterBody3D提供的神奇方法。只有在调用move_and_slide()或move_and_collide()之后,它才能根据碰撞结果准确判断角色是否站在地面上。因此,我们在计算重力时检查它,只有不在地面上时才应用重力。- 分离计算:将水平速度和垂直速度分开计算是个好习惯。这样逻辑更清晰,也便于后续单独修改(比如添加跳跃时,只需修改垂直速度)。
3.3 执行移动:move_and_slide()的魔力
计算好velocity后,最后一步就是调用move_and_slide()。
move_and_slide()这个方法做了很多事情:
- 内部移动:它根据当前
velocity向量,尝试移动角色。 - 碰撞检测与响应:如果移动过程中撞到其他
PhysicsBody(如墙壁、地板),它会根据碰撞面的法线自动调整velocity,防止穿透。例如,撞到水平的墙,水平速度会被抵消;撞到地板,垂直速度会被设置为0。 - 更新地面状态:移动完成后,它会更新内部碰撞状态,使得
is_on_floor()等方法能返回正确结果。 - 滑动处理:如果角色沿着斜坡或墙角移动,
move_and_slide()会尝试让角色“滑过”表面,而不是卡住。这是它比move_and_collide()更常用于角色移动的原因。
一个非常重要的细节:move_and_slide()可能会在内部修改velocity属性(例如,撞墙后水平速度被设为0)。因此,不要在调用move_and_slide()之后,在同一帧内再次使用velocity进行其他物理计算,除非你明确知道自己在做什么。通常的模式是:计算新的velocity-> 调用move_and_slide()-> 等待下一帧。
4. 完整实现与代码逐行解读
现在,我们把所有部分组合起来,形成一个完整的、可用的角色移动脚本。我将创建一个名为player.gd的脚本,附加到CharacterBody3D节点上。
4.1 脚本属性定义
首先,在脚本顶部定义一些可配置的属性。使用@export关键字可以让它们在编辑器的检查器中显示,方便随时调整,无需修改代码。
extends CharacterBody3D # 玩家在地面上的移动速度(米/秒) @export var speed: float = 10.0 # 空中的下落加速度(米/秒²)。地球重力约为 9.8,游戏中可以调大些让下落更快。 @export var fall_acceleration: float = 30.0 # 这是一个临时变量,用于存储我们计算出的目标速度,最后赋值给 `velocity`。 # 将其与 `velocity` 分开,可以避免直接修改 `velocity` 可能带来的混淆。 var target_velocity = Vector3.ZERO为什么用@export?调试和平衡游戏参数时,你可以在运行游戏的同时,在编辑器中实时拖动滑块修改speed和fall_acceleration,立即看到效果,这比反复修改代码、重启游戏高效得多。
4.2 物理处理函数_physics_process
这是脚本的核心。
func _physics_process(delta: float) -> void: # 1. 初始化输入方向向量 var input_direction = Vector3.ZERO # 2. 根据按键输入更新方向向量 # 注意:Godot 的 3D 坐标系,Z轴负方向是“向前”(看向屏幕里)。 if Input.is_action_pressed("move_right"): input_direction.x += 1.0 if Input.is_action_pressed("move_left"): input_direction.x -= 1.0 if Input.is_action_pressed("move_back"): input_direction.z += 1.0 if Input.is_action_pressed("move_forward"): input_direction.z -= 1.0 # 3. 处理方向:归一化并让角色面向移动方向 if input_direction != Vector3.ZERO: # 归一化,确保对角线移动速度不会更快 input_direction = input_direction.normalized() # 可选:让角色的视觉模型(例如一个子节点 MeshInstance3D)面向移动方向 # 这里假设你有一个叫 “Pivot” 或 “Model” 的子节点来控制视觉旋转 # $Pivot.look_at(global_position + input_direction, Vector3.UP) # 更简单的做法:直接设置 Basis。Basis.looking_at() 创建一个朝向目标方向的旋转。 $Pivot.basis = Basis.looking_at(input_direction) # 4. 计算地面(水平)速度 target_velocity.x = input_direction.x * speed target_velocity.z = input_direction.z * speed # 5. 计算垂直速度:应用重力 if not is_on_floor(): # 如果不在空中,则应用向下的加速度 # 注意:Y轴向上为正,所以重力是负加速度 target_velocity.y -= fall_acceleration * delta else: # 如果在地面上,确保垂直速度不被重力持续影响而变成负数(除非你想让角色沉入地面)。 # 通常,在地面时我们把垂直速度设为0或一个很小的值,防止抖动。 # move_and_slide() 在碰到地面时会自动将垂直速度设为0,但这里显式处理更安全。 target_velocity.y = 0.0 # 6. 将计算好的目标速度赋给角色的 velocity velocity = target_velocity # 7. 执行移动!这是最关键的一步。 move_and_slide()逐行解读与技巧:
- 第2步(输入处理):这里使用了四个独立的
if语句,而不是elif。这是为了允许组合按键(如同时按右和前),产生对角线输入。 - 第3步(面向方向):
$Pivot.basis = Basis.looking_at(input_direction)这行代码非常有用。它让一个名为Pivot的子节点(通常用来挂载角色模型)立即面向移动方向。Basis.looking_at(target_direction, up_vector)会计算出一个旋转矩阵,让节点的 -Z 轴(前向)指向target_direction。第二个参数Vector3.UP定义了哪个方向是“上”,确保旋转不会倒置。 - 第5步(重力处理):
if not is_on_floor():这个判断是平台游戏逻辑的基石。只有角色悬空时,我们才应用重力。一旦move_and_slide()检测到角色站在了地面上,is_on_floor()就会返回true,重力计算停止,角色稳稳站住。 - 第5步(地面垂直速度):将地面上的
target_velocity.y设为0是一个好习惯。虽然move_and_slide()在碰撞到地面时通常会处理,但显式设置可以避免一些极端情况下的微小下沉或抖动。 - 第7步(
move_and_slide()):调用这个方法后,角色的位置会被更新,碰撞状态(is_on_floor等)也会被刷新,供下一帧使用。
4.3 基础跳跃功能的实现
一个不能跳跃的角色是不完整的。让我们在现有基础上增加跳跃功能。
首先,在@export变量区域添加一个跳跃速度:
@export var jump_impulse: float = 15.0 # 跳跃的初始向上速度然后,在_physics_process函数中,重力计算部分之前,添加跳跃输入检测:
func _physics_process(delta: float) -> void: # ... [之前的输入和方向计算代码不变] ... # 4. 处理跳跃输入(必须在地面上才能跳) if Input.is_action_just_pressed("jump") and is_on_floor(): target_velocity.y = jump_impulse # 赋予一个向上的初速度 # 5. 计算地面速度(原第4步) target_velocity.x = input_direction.x * speed target_velocity.z = input_direction.z * speed # 6. 计算垂直速度:应用重力(原第5步,但逻辑需调整) # 注意:跳跃后,is_on_floor() 会立即变为 false,所以重力会开始作用。 if not is_on_floor(): target_velocity.y -= fall_acceleration * delta # 注意:这里不再有 else target_velocity.y = 0,因为跳跃已经设置了y速度。 # 但是,如果因为某种原因(如从边缘滑落)y速度是负的,而我们又在地面,应该重置。 # 更健壮的写法: # if is_on_floor() and target_velocity.y < 0: # target_velocity.y = 0 # elif not is_on_floor(): # target_velocity.y -= fall_acceleration * delta # 7. 赋值并移动(原第6、7步) velocity = target_velocity move_and_slide()跳跃逻辑的关键:
is_action_just_pressed(“jump”):这个函数只在按键被按下的那一帧返回true。如果用is_action_pressed,玩家按住空格键会导致角色不断向上“喷射”,这不是我们想要的单次跳跃。and is_on_floor():这是一个重要的限制条件,防止“二段跳”或空中无限跳(除非你的游戏设计允许)。只有在地面上时,按下跳跃键才有效。- 跳跃的物理:跳跃本质上就是给角色一个向上的初始速度 (
jump_impulse)。之后,重力 (fall_acceleration) 会持续将这个速度减小,直到变为负值,角色开始下落。
5. 常见问题、调试技巧与性能优化
即使代码看起来正确,在实际运行中你可能会遇到各种奇怪的问题。这里我总结了一些常见坑点和解决方案。
5.1 角色移动“滑冰”或停不下来
问题描述:松开按键后,角色还会缓慢滑动一段距离才停止。原因:你的水平速度计算逻辑可能没有在无输入时正确归零。检查input_direction是否为Vector3.ZERO,以及是否正确地将其乘以了speed。在我们的代码中,当没有输入时,input_direction是(0,0,0),那么target_velocity.x和.z就会被设为0,角色应该立刻停止。如果还在滑,可能是:
- 碰撞形状(CollisionShape)太圆滑或与地面摩擦力设置有关(但
CharacterBody3D默认不模拟摩擦力)。 - 你错误地修改了
velocity而没有通过target_velocity。确保每一帧都重新计算target_velocity.x和.z,而不是累加。
5.2 角色卡在斜坡或轻微不平的地面
问题描述:角色在斜坡上走不上去,或者在小凸起上被卡住。解决方案:move_and_slide()有一个非常实用的参数叫floor_max_angle(默认约 45 度)。它定义了多大角度的斜面可以被视为“地板”。如果你的斜坡角度超过了这个值,is_on_floor()会返回false,角色会滑下来。你可以根据需要调大这个值(比如到 60 度),或者在调用时传入:
move_and_slide() # 或者更明确地: # move_and_slide(velocity, Vector3.UP, false, 4, deg_to_rad(60.0))参数依次是:速度、上方向向量、是否在斜坡上停止、最大滑移次数、地板最大角度。通常使用默认值即可,但了解它们有助于调试。
5.3 角色抖动或穿模
问题描述:角色在移动,尤其是碰撞时,出现高频抖动,或者偶尔会穿过薄墙。原因与解决:
- Delta Time:确保所有速度、位移的计算都乘以了
delta(重力计算我们做了)。忘记乘delta会导致帧率越高移动越快,物理不稳定。 - 碰撞形状:确保
CollisionShape3D的大小和位置与视觉模型(Mesh)基本匹配。一个过大或过小的碰撞盒会导致奇怪的碰撞反馈。对于人形角色,一个胶囊体(CapsuleShape3D)通常比长方体更合适。 - 速度过大:如果一帧内移动的距离(
velocity * delta)大于角色碰撞体的尺寸,可能会发生“隧道效应”,直接穿过薄物体。CharacterBody3D的move_and_slide()内部有碰撞检测,但如果速度极快(比如子弹),可能需要使用move_and_collide()并手动处理射线投射。 - 物理帧率:确保移动逻辑在
_physics_process中,而不是_process。
5.4 性能优化小贴士
- 避免在
_physics_process中做繁重操作:这里的代码每物理帧(默认每秒60次)都会执行。复杂的数学计算、大量的节点查找(get_node)或资源加载应尽量避免。如果必须做,考虑缓存结果。 - 使用
@onready缓存节点引用:如果你需要频繁访问子节点(比如上面代码中的$Pivot),可以在脚本顶部用@onready缓存它,避免每次_physics_process都进行路径查找。
extends CharacterBody3D @onready var pivot_node = $Pivot # 游戏启动时缓存一次 func _physics_process(delta): # ... 之后使用 pivot_node 而不是 $Pivot ... if input_direction != Vector3.ZERO: pivot_node.basis = Basis.looking_at(input_direction)- 简化碰撞形状:在保证游戏体验的前提下,使用尽可能简单的碰撞形状(球体、胶囊体、长方体)。复杂的凸包或三角网格碰撞体计算开销大。
5.5 调试显示:让速度可视化
在开发过程中,将角色的速度向量打印出来或可视化,能极大帮助调试。
func _physics_process(delta): # ... 所有移动逻辑之后 ... # 在输出面板打印速度(调试用,发布时移除) # print("Velocity: ", velocity, " | On Floor: ", is_on_floor()) # 或者使用 Debug 覆盖层(如果启用了) # DebugDraw3D.draw_line(global_position, global_position + velocity, Color.RED)Godot 4 有内置的DebugDraw3D插件(需在项目设置中启用),可以非常方便地在3D视图中绘制线条、文字,用于调试位置、速度向量等。
6. 扩展与进阶:让移动更丰富
基础移动实现后,你可以在此基础上添加更多功能,让角色操控感更上一层楼。
6.1 冲刺与疾跑
增加一个冲刺功能,通常是在移动时按住某个键(如 Shift)来获得更高的速度。
@export var sprint_speed_multiplier: float = 1.8 @export var sprint_acceleration: float = 20.0 # 加速到冲刺速度的力度 var current_speed: float = 0.0 # 当前实际速度,用于平滑加速 func _physics_process(delta): var input_dir = Vector3.ZERO # ... 获取输入方向 ... var target_speed = speed if Input.is_action_pressed("sprint"): target_speed *= sprint_speed_multiplier # 平滑加速/减速(线性插值) current_speed = lerp(current_speed, target_speed, sprint_acceleration * delta) if input_dir != Vector3.ZERO: input_dir = input_dir.normalized() $Pivot.look_at(global_position + input_dir, Vector3.UP) # 使用平滑后的速度 target_velocity.x = input_dir.x * current_speed target_velocity.z = input_dir.z * current_speed # ... 重力、跳跃、移动 ...这里引入了lerp(线性插值)函数来实现速度的平滑过渡,让起步和停止不那么生硬。
6.2 摄像机跟随与第三人称控制
基础的移动是让角色自己转。在第三人称游戏中,通常摄像机是独立的,角色应朝向摄像机的前方移动。
思路:
- 获取摄像机(或一个摄像机支架节点)的全局变换(
global_transform)。 - 将输入方向(基于键盘的“前、后、左、右”)从这个摄像机的视角空间,转换到世界空间。
- 角色朝这个“世界空间方向”移动和旋转。
@onready var camera_pivot = get_node("../CameraPivot") # 假设摄像机支架是兄弟节点 func _physics_process(delta): var input_dir = Vector3.ZERO if Input.is_action_pressed("move_forward"): input_dir.z -= 1 if Input.is_action_pressed("move_back"): input_dir.z += 1 if Input.is_action_pressed("move_left"): input_dir.x -= 1 if Input.is_action_pressed("move_right"): input_dir.x += 1 # 关键转换:将局部输入方向转换为相对于摄像机的世界方向 if input_dir != Vector3.ZERO: input_dir = input_dir.normalized() # 获取摄像机朝向,但忽略其俯仰(pitch)旋转,只保留水平(Y轴)旋转 var camera_basis = camera_pivot.global_transform.basis var camera_forward = -camera_basis.z # 摄像机的前向是 -Z camera_forward.y = 0 camera_forward = camera_forward.normalized() var camera_right = camera_basis.x camera_right.y = 0 camera_right = camera_right.normalized() # 计算最终的世界移动方向 var world_dir = camera_forward * input_dir.z + camera_right * input_dir.x world_dir = world_dir.normalized() # 让角色面向移动方向 $Pivot.look_at(global_position + world_dir, Vector3.UP) target_velocity.x = world_dir.x * speed target_velocity.z = world_dir.z * speed else: target_velocity.x = 0 target_velocity.z = 0 # ... 重力、跳跃、移动 ...这个逻辑稍微复杂,但它是第三人称动作游戏(如《黑暗之魂》、《战神》)控制的基础。核心思想是:玩家的“前”不再是角色的正前方,而是摄像机的水平前向。
6.3 动画状态机集成
移动逻辑完成后,下一步就是连接动画。通常你会有一个AnimationPlayer或AnimationTree节点来控制角色的行走、奔跑、跳跃、空闲等动画。
在_physics_process中,根据当前状态(速度大小、是否在地面等)触发动画:
@onready var animation_player = $AnimationPlayer func _physics_process(delta): # ... 所有移动逻辑 ... move_and_slide() # 移动后,根据状态播放动画 var is_moving = input_direction.length_squared() > 0.01 and is_on_floor() var is_in_air = not is_on_floor() if is_in_air: if velocity.y > 0: animation_player.play("jump_up") else: animation_player.play("jump_down") elif is_moving: if Input.is_action_pressed("sprint"): animation_player.play("run") else: animation_player.play("walk") else: animation_player.play("idle")对于更复杂的动画混合(如从走到跑的平滑过渡),建议使用AnimationTree节点配合状态机(AnimationNodeStateMachine),这提供了更强大和灵活的控制。
7. 总结与个人心得
控制角色移动是游戏编程中最基础也最有趣的部分之一。从按下键盘到角色在屏幕上响应,这中间涉及了输入处理、向量数学、物理集成和状态管理。Godot 的CharacterBody3D和move_and_slide()将这些复杂性封装得很好,让你能专注于游戏性的逻辑。
在我自己的项目实践中,有几点深刻体会:
- 参数化一切:像
speed、jump_impulse、fall_acceleration这样的数值,一定要做成@export变量。在游戏测试阶段,微调这些参数是家常便饭,能在编辑器里实时调整会节省大量时间。 - 理解向量:游戏开发本质上是向量和矩阵的运算。花点时间理解
Vector3的加减、缩放、点乘、叉乘和归一化,这些知识在处理移动、朝向、视线、碰撞检测时无处不在。 - 调试是朋友:不要害怕使用
print()输出变量的值,或者用DebugDraw3D画线。亲眼看到速度向量的方向和长度,比在脑子里想象要直观一百倍。 - 从简单开始:先实现最基础的、无重力、无旋转的八方向移动。确保它工作正常后,再一步步添加重力、跳跃、摄像机相对移动、动画等。每加一个功能就测试一下,能帮你快速定位问题。
最后,别忘了享受这个过程。当你第一次按下按键,看到自己创建的角色在亲手搭建的世界里奔跑、跳跃时,那种成就感是无与伦比的。这仅仅是开始,有了这个可操控的角色,你已经打开了通往更复杂游戏机制(战斗、对话、解谜、载具)的大门。祝你开发愉快!