1. 项目概述:为什么我们需要一个模块化的角色控制器?
如果你正在用Godot 4做3D游戏,角色控制器大概率是你遇到的第一个,也是最头疼的“拦路虎”。网上能找到的教程,要么是简单到只能前后左右移动的“玩具”,要么是把所有代码塞进一个脚本的“意大利面条”,一旦你想加个下蹲、冲刺或者攀爬功能,就得在成百上千行代码里大海捞针,最后要么推倒重来,要么干脆放弃。
这就是我决定从头梳理并构建一个模块化3D角色控制器的初衷。它不是一个固定的、不可变的“黑盒”,而是一个像乐高积木一样可以自由拼装的系统。核心目标就两个:一是让功能解耦,二是让逻辑清晰。动画状态机负责所有动画的播放和过渡,输入管理器统一处理键盘、手柄信号,物理控制器专心处理移动、跳跃、碰撞,摄像机模块独立运作。每个模块只做一件事,并且通过定义良好的接口与其他模块通信。
这样做的好处是显而易见的。当你需要调整跳跃手感时,你只需要去PhysicsController里修改几个参数;当你想为角色添加一个新的“眩晕”状态时,你只需要在AnimationStateMachine里新增一个状态节点和几条过渡规则,完全不用去碰移动逻辑。对于团队协作来说,美术和动画师可以专注于状态机的搭建,而程序员则可以埋头优化物理和输入逻辑,互不干扰。
所以,这篇内容不是教你复制粘贴一段“万能代码”,而是分享一套经过实战检验的设计方法论和实现细节。无论你是想做一款快节奏的FPS,还是需要复杂交互的RPG,这套模块化框架都能为你提供一个坚实、可扩展的起点。
2. 核心架构设计:模块化如何落地?
模块化不能停留在概念上,必须在项目结构、节点组织和代码架构上体现出来。在Godot中,我们充分利用场景(Scene)和节点(Node)的继承与组合特性来构建我们的控制器。
2.1 场景与节点树结构
我们的主角Player将是一个继承自CharacterBody3D的场景。但CharacterBody3D本身只提供物理属性和_physics_process回调,我们不会把任何业务逻辑直接写在这里。它的角色更像是一个“容器”或“根骨骼”。
一个推荐的角色控制器节点树结构如下:
Player (CharacterBody3D) ├── CollisionShape3D (胶囊体碰撞体) ├── MeshInstance3D (角色模型/骨架) ├── CameraPivot (Node3D) │ └── Camera3D (主摄像机) ├── InputManager (Node) # 输入处理模块 ├── PhysicsController (Node) # 物理与移动逻辑模块 └── AnimationStateMachine (AnimationTree) # 动画状态机模块为什么这样设计?
CameraPivot独立存在:将摄像机挂载在一个独立的Node3D下,而不是直接挂在Player根节点上,是为了实现更灵活的摄像机控制。我们可以通过旋转CameraPivot来实现鼠标Y轴(上下看)的视角控制,而通过旋转Player根节点来实现X轴(左右转身)控制,两者解耦,避免万向节死锁。- 模块作为子节点:
InputManager、PhysicsController这些模块本身是普通的Node。它们通过@export变量暴露配置参数,并通过调用父节点(即Player)的方法或直接修改Player的属性(如velocity)来发挥作用。这种设计让模块的职责边界非常清晰。 AnimationTree作为核心:AnimationStateMachine本质上是一个配置好的AnimationTree节点。我们将AnimationPlayer中的动画导入到AnimationTree中,并在其中可视化地编辑状态机。这是Godot处理复杂角色动画的官方推荐方案,功能强大且直观。
2.2 模块间的通信机制:信号与依赖注入
模块化之后,最大的挑战是如何让这些独立的模块协同工作。我们主要采用两种方式:
信号(Signals)- 用于松散耦合的事件通知这是Godot的核心通信机制,非常适合“发生了某件事,需要通知其他模块”的场景。
- 例如:
InputManager检测到玩家按下了跳跃键,它不应该直接调用PhysicsController.jump(),而是应该发出一个自定义信号jump_just_pressed。PhysicsController连接到这个信号,并在自己的回调函数里执行跳跃逻辑。 - 再例如:
PhysicsController的_physics_process中,根据计算出的速度,每帧发出一个velocity_updated信号,并携带当前的velocity向量。AnimationStateMachine连接到这个信号,根据速度的大小和方向来驱动状态机中的参数(如blend_position),从而决定播放行走、奔跑还是 idle 动画。
实操心得:定义信号时,尽量使用描述事件本身的名称(如
jump_just_pressed,landed,state_changed),而不是描述接收方应该做什么(如request_jump)。前者更符合信号的“通知”本质,让模块更独立。- 例如:
通过父节点引用(轻量级依赖注入)- 用于紧密协作的数据共享对于一些需要频繁访问或紧密协作的模块,通过父节点获取引用是更直接的方式。通常我们在模块的
_ready()函数中获取引用。- 例如:
PhysicsController需要知道角色的CollisionShape3D来判断是否着地(is_on_floor())。我们可以在PhysicsController中定义@export var collision_shape: CollisionShape3D,然后在编辑器中拖拽赋值,或者在_ready()中用get_node(“../CollisionShape3D”)获取。 - 例如:
AnimationStateMachine需要直接控制MeshInstance3D的骨架来播放动画,它可以通过@export引用到MeshInstance3D。
注意事项:尽量避免模块之间互相持有引用(A引用B,B又引用A),这会造成循环依赖,增加复杂度和调试难度。通信流向应尽量是单向的或通过父节点中转。
- 例如:
2.3 动画状态机(AnimationTree)的设计哲学
AnimationTree是Godot动画系统的集大成者,而AnimationNodeStateMachine是其灵魂。我们的目标是建立一个能清晰反映角色“状态”的机器。
一个基础的3D角色状态机可能包含以下状态:
Idle(待机)Walk(行走)Run(奔跑)Jump(跳跃上升)Fall(下落)Land(着陆)
状态过渡(Transition)是状态机的关键。我们不应该用代码硬编码“如果速度>5,则播放奔跑动画”,而应该在状态机中创建从Idle到Walk、Walk到Run的过渡,并设置过渡条件。这些条件由我们通过代码设置的参数(parameters)来驱动。
在AnimationTree中,我们可以创建如下参数:
blend_position/blend_space: 用于在Idle、Walk、Run等移动动画间平滑混合。通常是一个二维向量,表示速度在角色局部空间下的X(左右)和Z(前后)分量。Jumping(布尔值): 为真时,从任何状态过渡到Jump状态。Falling(布尔值): 为真时,从Jump状态过渡到Fall状态。is_on_floor(布尔值): 着陆时,触发从Fall到Land再到Idle的过渡。
模块化在这里的体现:AnimationStateMachine模块的唯一职责,就是根据从PhysicsController等模块接收到的信号或数据,更新AnimationTree的这些参数。它不关心速度是如何计算出来的,只关心“当前速度是多少”。这种关注点分离让动画逻辑变得极其清晰和可维护。
3. 核心模块深度实现
理论说完了,我们进入实战环节,看看每个模块具体怎么写。
3.1 InputManager:统一且可配置的输入处理
输入管理的目标是抽象化输入设备,提供统一的“动作(Action)”接口。Godot内置的“项目设置 -> 输入映射”是我们的好帮手,但我们需要一个管理器来封装它。
# InputManager.gd extends Node # 导出变量,方便在编辑器中调整按键映射(备用方案) @export var move_forward_action := "move_forward" @export var move_backward_action := "move_backward" @export var move_left_action := "move_left" @export var move_right_action := "move_right" @export var jump_action := "jump" @export var sprint_action := "sprint" @export var crouch_action := "crouch" # 信号定义 signal move_input_received(direction: Vector3) # 移动输入,方向已标准化 signal jump_just_pressed() signal jump_just_released() signal sprint_just_pressed() signal crouch_just_pressed() var _raw_input_vector := Vector2.ZERO func _process(_delta: float) -> void: # 1. 处理移动输入(在_process中处理更即时) var input_vector := Vector2.ZERO input_vector.x = Input.get_axis(move_left_action, move_right_action) input_vector.y = Input.get_axis(move_forward_action, move_backward_action) # 限制输入向量长度不超过1(防止斜向移动更快) _raw_input_vector = input_vector.limit_length(1.0) # 将2D输入转换为3D方向(基于当前摄像机朝向),但转换应在PhysicsController中结合摄像机进行 # 这里只发射原始向量,让PhysicsController决定如何转换 emit_signal("move_input_received", Vector3(_raw_input_vector.x, 0, _raw_input_vector.y)) # 2. 处理瞬时动作输入 if Input.is_action_just_pressed(jump_action): emit_signal("jump_just_pressed") if Input.is_action_just_released(jump_action): emit_signal("jump_just_released") if Input.is_action_just_pressed(sprint_action): emit_signal("sprint_just_pressed") if Input.is_action_just_pressed(crouch_action): emit_signal("crouch_just_pressed")关键点解析:
- 使用
Input.get_axis:处理像移动这样有正负方向的输入,它自动处理了按键冲突(同时按左右结果为0),比分别判断is_action_pressed更简洁。 - 在
_process中处理:输入检测放在_process中能获得更快的响应,尤其是对于just_pressed这类瞬时事件。而物理移动逻辑仍在_physics_process中。 - 分离原始输入与逻辑:
InputManager只负责收集和初步处理输入,然后通过信号发出。它不关心这个“前进”输入最终是让角色移动,还是让飞船加速。这种设计让输入系统可以被任何需要输入的实体复用。
3.2 PhysicsController:真实的物理与手感调校
这是角色控制器的核心,所有移动、跳跃、重力、碰撞的逻辑都在这里。手感的好坏,90%取决于这个模块的调校。
# PhysicsController.gd extends Node # 可调参数,全部导出到编辑器,方便实时调试 @export var max_speed := 5.0 @export var acceleration := 10.0 # 加速度,影响提速快慢 @export var deceleration := 15.0 # 减速度,影响停下快慢 @export var jump_velocity := 4.5 # 跳跃初速度 @export var gravity := 9.8 * 2.0 # 重力倍数,Godot默认重力偏小,通常需要加倍 @export var air_control_factor := 0.3 # 空中控制系数(0-1),1表示与地面控制力相同 @export var sprint_multiplier := 1.8 # 奔跑速度倍数 # 依赖引用 @export var character_body: CharacterBody3D @export var camera_pivot: Node3D # 内部状态 var _current_velocity := Vector3.ZERO var _is_sprinting := false var _wish_direction := Vector3.ZERO # 期望移动方向 func _ready() -> void: if not character_body: character_body = get_parent() as CharacterBody3D # 连接到InputManager的信号 var input_manager = get_node("../InputManager") if input_manager: input_manager.move_input_received.connect(_on_move_input) input_manager.jump_just_pressed.connect(_on_jump_input) input_manager.sprint_just_pressed.connect(_on_sprint_input) func _physics_process(delta: float) -> void: if not character_body: return # 1. 应用重力(始终生效,除非在特殊状态如攀爬) if not character_body.is_on_floor(): _current_velocity.y -= gravity * delta # 2. 处理水平移动 var target_speed = max_speed * (_is_sprinting ? sprint_multiplier : 1.0) var horizontal_velocity = Vector3(_current_velocity.x, 0, _current_velocity.z) var horizontal_direction = horizontal_velocity.normalized() var current_speed = horizontal_velocity.length() # 计算期望速度 var target_velocity = _wish_direction * target_speed # 平滑插值当前速度至目标速度 var speed_diff = target_velocity - horizontal_velocity var accel = acceleration if _wish_direction.dot(horizontal_direction) > 0 else deceleration # 空中控制力减弱 if not character_body.is_on_floor(): accel *= air_control_factor horizontal_velocity = horizontal_velocity.move_toward(target_velocity, accel * delta) _current_velocity.x = horizontal_velocity.x _current_velocity.z = horizontal_velocity.z # 3. 处理跳跃(仅在着地时允许) # 注意:跳跃速度的施加已经在_on_jump_input信号回调中完成 # 4. 将最终速度赋予CharacterBody3D并移动 character_body.velocity = _current_velocity character_body.move_and_slide() # 5. 移动后,更新_current_velocity(因为move_and_slide可能会修改velocity.y) _current_velocity = character_body.velocity # 6. 发出信号,通知其他模块(如动画状态机)速度已更新 emit_signal("velocity_updated", _current_velocity, character_body.is_on_floor()) func _on_move_input(raw_direction_3d: Vector3) -> void: # 将基于输入的原始方向,转换为基于摄像机朝向的世界空间方向 if camera_pivot: # 获取摄像机水平朝向(忽略上下俯仰) var camera_basis = camera_pivot.global_transform.basis var camera_forward = -camera_basis.z # Godot中-Z是摄像机前方 var camera_right = camera_basis.x camera_forward.y = 0 camera_right.y = 0 camera_forward = camera_forward.normalized() camera_right = camera_right.normalized() # 组合方向 _wish_direction = (camera_forward * raw_direction_3d.z) + (camera_right * raw_direction_3d.x) _wish_direction.y = 0 if _wish_direction.length() > 0: _wish_direction = _wish_direction.normalized() else: # 如果没有摄像机,则使用角色自身朝向(适用于俯视角等) _wish_direction = raw_direction_3d func _on_jump_input() -> void: if character_body and character_body.is_on_floor(): _current_velocity.y = jump_velocity func _on_sprint_input() -> void: _is_sprinting = not _is_sprinting # 切换奔跑状态手感调校核心:
- 加速度与减速度:这是实现“手感”的关键。
acceleration决定从静止加速到最大速度的快慢,值越大,响应越快,感觉越“灵敏”。deceleration决定从运动到停止的快慢,值越大,停下越干脆。不同的游戏类型需要不同的搭配:街机风格游戏可能加速度和减速度都很大;拟真游戏则加速度较小,减速度可能还受地面摩擦力影响。 - 空中控制:
air_control_factor是一个非常重要的参数。设置为0,角色起跳后完全无法改变水平方向;设置为1,则和地面控制力一样。对于平台跳跃游戏,通常设置为一个较小的值(如0.2-0.5),以增加跳跃的挑战性和确定性。 - 速度继承:注意我们在
_physics_process最后将character_body.velocity赋值回_current_velocity。这是因为move_and_slide()方法在发生碰撞时,可能会修正速度向量(特别是Y轴)。我们必须使用修正后的速度进行下一帧的计算,才能保证物理模拟的连贯性。
3.3 AnimationStateMachine:驱动视觉表现
这个模块是动画师和程序员之间的桥梁。它接收物理数据,驱动复杂的动画状态机。
首先,在场景中设置AnimationTree:
- 为你的角色模型添加
AnimationPlayer并导入所有动画(Idle, Walk, Run, JumpStart, JumpLoop, Fall, Land)。 - 添加一个
AnimationTree节点。在它的属性中,将Animation Player路径指向你的AnimationPlayer,然后将Tree Root的类型设置为AnimationNodeStateMachine,并激活AnimationTree。 - 双击
AnimationTree节点打开状态机编辑器。创建状态(如Idle,Walk,Run),并将AnimationPlayer中的动画拖拽赋值给每个状态。 - 创建状态之间的过渡(Transition),并为其设置条件。
然后,我们编写脚本去驱动它:
# AnimationStateMachine.gd extends AnimationTree # 在编辑器中关联AnimationTree的参数名 @export var blend_position_param := "parameters/BlendSpace2D/blend_position" @export var jumping_param := "parameters/conditions/is_jumping" @export var falling_param := "parameters/conditions/is_falling" @export var landing_param := "parameters/conditions/is_landing" @export var is_on_floor_param := "parameters/conditions/is_on_floor" func _ready() -> void: # 连接到PhysicsController的信号 var physics_controller = get_node("../PhysicsController") if physics_controller: physics_controller.velocity_updated.connect(_on_velocity_updated) func _on_velocity_updated(velocity: Vector3, is_on_floor: bool) -> void: # 1. 更新移动混合空间参数 # 将世界空间速度转换到角色的局部空间 var parent = get_parent() if parent: var local_velocity = parent.global_transform.basis.inverse() * velocity # 我们只关心X和Z分量(水平移动) var blend_pos = Vector2(local_velocity.x, local_velocity.z) # 可以限制一下最大值,避免混合位置跑得太远 blend_pos = blend_pos.limit_length(1.0) set(blend_position_param, blend_pos) # 2. 更新状态条件参数 set(is_on_floor_param, is_on_floor) # 3. 更复杂的状态逻辑:跳跃、下落、着陆 # 这里需要一个简单的内部状态机来管理,因为Godot的StateMachine需要布尔值触发 # 我们可以用之前的velocity和is_on_floor来判断 if not is_on_floor and velocity.y > 0.1: set(jumping_param, true) set(falling_param, false) set(landing_param, false) elif not is_on_floor and velocity.y < -0.1: set(jumping_param, false) set(falling_param, true) set(landing_param, false) elif is_on_floor: # 刚着地的一帧,触发着陆动画 if get(falling_param) == true: # 如果之前在下落 set(landing_param, true) # 着陆动画播放完后,需要通过AnimationTree的动画完成信号自动跳回Idle/Walk/Run set(jumping_param, false) set(falling_param, false)动画状态机设计技巧:
- 使用BlendSpace2D:对于
Idle、Walk、Run,不要用三个独立的状态硬切换,强烈建议使用AnimationNodeBlendSpace2D。你可以创建一个2D混合空间,X轴代表水平速度(左/右),Y轴代表前后速度。将Idle动画放在(0,0),Walk动画放在(0,1),Run动画放在(0,2)等位置。这样,通过更新blend_position参数,动画就能根据速度向量平滑混合,实现从走到跑的无缝过渡,以及八方向移动。 - 条件参数(Conditions):状态之间的过渡条件,在状态机编辑器中设置为
parameters/conditions/xxx。我们在代码中通过set(“parameters/conditions/is_jumping”, true)来触发过渡。注意,这些条件是布尔值,通常用于触发一次性的状态转移(如按下跳跃键时,is_jumping设为true,触发进入Jump状态;进入Jump状态后,应立刻将其设回false,避免重复触发)。 - 动画信号:
AnimationTree和AnimationPlayer可以发出动画开始、结束的信号。这对于处理像“着陆”(Land)这样的短暂状态非常有用。你可以让Land状态连接到一个animation_finished信号,播放完毕后自动过渡回Idle状态。
4. 进阶功能与模块扩展
一个基础控制器搭建完成后,我们可以轻松地通过添加新模块或扩展现有模块来增加功能。
4.1 实现下蹲(Crouch)功能
- 在InputManager中:添加
crouch_just_pressed信号(如上文代码所示)。 - 在PhysicsController中:
- 添加
@export var crouch_speed_multiplier := 0.5和@export var crouch_height := 1.0(假设正常高度2.0)。 - 添加
var _is_crouching := false状态变量。 - 连接
crouch_just_pressed信号,在回调中切换_is_crouching状态,并相应地修改CollisionShape3D的高度和位置,以及移动速度上限。 - 在
_physics_process中,根据_is_crouching调整target_speed。
- 添加
- 在AnimationStateMachine中:
- 在状态机中添加
Crouch_Idle和Crouch_Walk状态。 - 添加一个
is_crouching参数。 - 根据该参数和速度,决定是过渡到站立状态还是蹲伏状态。
- 在状态机中添加
4.2 实现摄像机碰撞检测与抖动处理
基础的摄像机跟随可能会穿墙。我们需要一个更健壮的CameraController模块。
# CameraController.gd extends Node @export var target: Node3D # 跟随的目标(通常是Player根节点) @export var camera_pivot: Node3D # 控制俯仰的Pivot节点 @export var camera: Camera3D # 实际的摄像机 @export var distance_from_target := 4.0 @export var collision_mask := 1 # 用于射线检测的碰撞层 var _current_distance := distance_from_target func _physics_process(_delta: float) -> void: if not target or not camera: return # 1. 从目标位置(可以加一个偏移量,如Vector3.UP * 1.5)向摄像机当前位置发射射线 var from = target.global_transform.origin + Vector3.UP * 1.5 var to = camera_pivot.global_transform.basis * Vector3(0, 0, distance_from_target) # 摄像机相对Pivot的期望位置 to = from - to # 计算世界空间中的目标点 var space_state = target.get_world_3d().direct_space_state var query = PhysicsRayQueryParameters3D.create(from, to, collision_mask) query.exclude = [target] # 排除目标自身,避免检测到自己 var result = space_state.intersect_ray(query) # 2. 如果射线击中了什么,将摄像机拉近到碰撞点前方一点的位置 if result: _current_distance = min(distance_from_target, from.distance_to(result.position) - 0.5) # 留出0.5米缓冲 else: _current_distance = distance_from_target # 3. 平滑插值摄像机当前距离到目标距离,避免突兀跳动 camera.position.z = lerp(camera.position.z, -_current_distance, 0.2)摄像机手感优化:
- 鼠标平滑与灵敏度:在
_input事件中处理鼠标移动,使用lerp或smoothstep对输入的鼠标增量进行平滑处理,可以消除抖动感。 - 摄像机滞后(Lag):让
CameraPivot的旋转和位置不是立即跟随玩家,而是以一定的延迟平滑插值过去。这能带来更柔和、更电影感的运镜效果,尤其在高速移动时。
4.3 动画状态机的进阶:子状态机与混合树
对于更复杂的角色(如拥有武器、受伤、交互等状态),单一的状态机会变得臃肿。这时可以使用子状态机(Sub State Machine)。
- 将移动相关状态(Idle, Walk, Run, Crouch)打包成一个“Locomotion”子状态机。
- 创建独立的“Combat”子状态机,管理拔枪、瞄准、射击、换弹等状态。
- 创建独立的“Interaction”子状态机,管理开门、拾取等状态。
在根状态机中,通过一个“上层”参数(如mode,可以是LOCOMOTION,COMBAT,INTERACTION)来决定当前激活哪个子状态机。这实现了状态的分层管理,逻辑清晰度大幅提升。
此外,对于像“转身”这样的动画,可以使用动画混合树(AnimationNodeBlendTree)。例如,一个Turn节点,根据角色转身角速度,混合“左转90度”、“右转90度”、“原地转身”等多个动画片段,实现自然的转身过渡。
5. 调试技巧与性能优化
开发过程中,可视化调试和性能监控至关重要。
调试绘制(Debug Draw):
# 在PhysicsController的_process中绘制速度向量和期望方向 func _process(delta): if Engine.is_editor_hint(): # 仅在编辑器或调试模式下绘制 DebugDraw3D.draw_line(character_body.global_transform.origin, character_body.global_transform.origin + _current_velocity, Color.GREEN) DebugDraw3D.draw_line(character_body.global_transform.origin, character_body.global_transform.origin + _wish_direction * 2, Color.RED)你需要一个第三方调试绘制插件(如
godot-debug-draw-3d),它能帮助你在游戏运行时清晰地看到力的方向、射线检测结果等,对于调校手感有奇效。导出参数实时调校:将
PhysicsController中所有影响手感的参数(max_speed,acceleration,jump_velocity,gravity等)都标记为@export。在游戏运行时,暂停游戏,直接在编辑器的“远程”选项卡中调整这些参数,效果会实时反映,这是最快的调参方式。性能分析:Godot内置的性能分析器(
Debugger -> Profiler)是你的好朋友。重点关注:_physics_process耗时:确保你的物理计算不会成为瓶颈。复杂的射线检测、过多的move_and_slide调用(如果你有多个角色)都会增加负担。- 动画更新耗时:复杂的
AnimationTree状态机和大量的骨骼节点会影响性能。对于非主角的NPC,可以考虑使用更简单的动画系统(如AnimationPlayer直接播放)。 - Draw Call:确保角色模型材质合批得当,避免不必要的渲染开销。
常见问题排查表:
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 角色移动“打滑”或停不下来 | deceleration值太小;move_and_slide()后未正确更新速度 | 增大deceleration;检查是否将character_body.velocity赋值回内部速度变量。 |
| 跳跃手感“绵软”或下坠太快 | jump_velocity或gravity值不合适 | 调整jump_velocity(增加跳更高)和gravity(减小下落慢)。参考公式:跳跃高度 ≈jump_velocity^2 / (2 * gravity)。 |
| 动画切换生硬或错误 | 状态机条件参数设置错误;过渡规则有误 | 打开AnimationTree编辑器,检查每个过渡的条件表达式;在代码中打印驱动状态的参数值,看是否符合预期。 |
| 摄像机穿墙或抖动 | 碰撞检测射线参数不对;平滑插值系数不合适 | 绘制调试射线,检查from和to位置;调整射线检测的collision_mask;微调摄像机距离的平滑lerp系数。 |
| 输入有延迟 | 在_physics_process中处理输入 | 将瞬时输入(just_pressed)检测移到_process中。 |
| 斜向移动更快 | 输入向量未标准化 | 确保在InputManager中使用input_vector.limit_length(1.0)。 |
构建一个模块化的Godot 4 3D角色控制器,初期投入的时间会比写一个“大杂烩”脚本要多。但当你需要添加第二个、第三个角色类型,或者为现有角色增加一个“滑铲”或“爬墙”能力时,模块化设计的优势就会爆发式地体现出来。你几乎不需要修改原有代码,只需要像搭积木一样组合或替换模块。这种清晰、可维护、可扩展的代码结构,是任何严肃游戏项目走向成功的基石。