用 Godot 数学库实现角色移动、朝向判断与相机跟随:从 2D 到 3D 的完整指南
【免费下载链接】godot-docsGodot Engine official documentation项目地址: https://gitcode.com/GitHub_Trending/go/godot-docs
如果你刚接触 GDScript,第一次写"让角色动起来"的需求时,可能只需要一行position += velocity * delta,就能跑起来。但接下来每个需求都会撞上数学:斜向移动为什么比单方向快?角色什么时候算"面向"了敌人?相机怎么平滑地追着目标转?这篇 Godot 数学库完整指南从这些真实任务出发,带你认识 Godot 4.x 里最常用的一组数学工具:Vector2/Vector3 向量、Transform2D/Transform3D 变换矩阵、点积/叉积,以及 3D 里的四元数。不需要线性代数基础,每节都给你一段能直接跑的最小示例。
2D 任务一:让角色平滑移动,斜着走不加速
要解决的问题:读方向键或摇杆,让角色以恒定速度移动;但按下两个键时(比如"上+右"),角色不能变成 √2 倍速。
extends Node2D @export var speed := 200.0 func _physics_process(delta: float) -> void: # 从输入构建 0~1 范围的方向向量(Vector2 向量运算入门) var input_vec := Vector2( Input.get_action_strength("move_right") - Input.get_action_strength("move_left"), Input.get_action_strength("move_down") - Input.get_action_strength("move_up") ) if input_vec.length_squared() > 0.0: # normalized() 把向量压到长度 1,斜向和直走速度一致 var direction := input_vec.normalized() position += direction * speed * delta用到的 Godot 数学库核心就三件事:
Vector2(x, y):用两个数表示方向+大小。注意 Godot 的 2D 屏幕坐标里Y 轴向下,Vector2(0, 1)是朝下,不是朝上。.length()/.length_squared():向量的模长。只判断"远不远"时用length_squared(),可以省掉一次开方。.normalized():得到单位向量,即"只保留方向、长度归一"。
官方参考:classes/class_vector2.rst 里列出了全部方法,dot()、angle_to()、lerp()都在同一页。
顺手解决"转身":平滑转向目标
角色移动了,贴图却还朝着旧方向。2D 里旋转就是一个角度,用lerp_angle做平滑转向:
# 在上一段代码后面加: if input_vec.length_squared() > 0.0: var direction := input_vec.normalized() position += direction * speed * delta # 目标朝向:Vector2 的 angle_to 给出到目标的角度 var target_angle := direction.angle() # lerp_angle 会自动走角度里短的那条路,不会出现原地打转 rotation = lerp_angle(rotation, target_angle, 12.0 * delta)angle_to、lerp_angle这两个方法专门为"角色转向"这类需求准备,写法比手写角度差值简洁得多。
任务二:判断角色是否面向目标,点积就够了
要解决的问题:敌人 AI 想"看到"玩家才发起攻击。你手里有角色当前位置、朝向,和玩家位置。
直觉做法是算出"到玩家的向量"和"朝向"的夹角再比较,但那要先acos。Godot 数学库里有个更便宜的判断——点积(dot product):
func can_see_player(player_pos: Vector2) -> bool: var to_player := (player_pos - global_position).normalized() # 点积范围 -1 ~ 1:1 表示同向,0 垂直,-1 反向 return to_player.dot(direction) > cos(PI * 0.35) # 约 63° 的视锥内a.dot(b)在两个单位向量之间等于夹角的余弦,所以"点积大于某个阈值"就等价于"夹角小于某个角度",全程不用acos。
需要法线或左右侧时,用叉积
3D 里更常见的是:已知碰撞的两个边,求表面的法线方向。这时用叉积(cross product):
# vertex1/2/3 是三角面三个顶点(Vector3) var edge_a := vertex2 - vertex1 var edge_b := vertex3 - vertex1 var normal := edge_a.cross(edge_b).normalized()叉积的结果垂直于两个输入向量所在平面,是"算表面法线"的标准套路。2D 版本a.cross(b)返回的是标量,正负值可以直接告诉你 b 在 a 的顺时针还是逆时针一侧,常用于判定"目标在我左手边还是右手边"。
更多细节见 classes/class_vector3.rst。
任务三:搞懂 GDScript 变换矩阵,子节点跟着父节点走
要解决的问题:把"旋转 30° + 放大 1.5 倍 + 移到 (100, 50)"这类描述组合起来,并让子节点的transform自动跟着父节点传递。
Godot 4.x 中,2D 节点用Transform2D,3D 节点用Transform3D,两者都由两部分组成:
# 组合一个 2D 变换:先旋转,再缩放,最后给位置 var t := Transform2D() t = t.rotated(PI / 6) # 旋转 30° t = t.scaled(Vector2.ONE * 1.5) # 整体放大 1.5 倍 t.origin = Vector2(100, 50) # 放到指定位置 $Sprite2D.transform = t3D 里Transform3D的basis就是三根轴向量(X/Y/Z),单位矩阵下它们正好对应世界三轴:
局部变换如何变成全局变换
节点树里每个节点只存局部变换(相对于父节点)。引擎沿节点树把各级transform连乘起来,才得到全局变换。你自己组合时同理,用*号即可:
# 把局部变换换算到世界空间:父级变换 * 局部变换 var local := Transform2D() local.origin = Vector2(50, 0) # 局部:向右 50 var world := get_parent().transform * local # 乘上父节点变换 # 反方向:把世界坐标换算回某节点局部坐标 var world_pos := Vector3(10, 0, -5) var local_pos := get_node("Enemy").transform.affine_inverse() * world_pos3D 的写法完全一致,只是换成Transform3D。affine_inverse()给你一个"反向变换",是坐标换算最常用的技巧。这两个类型的完整方法表在 classes/class_transform2d.rst 和 classes/class_transform3d.rst。
任务四:进入 3D,用四元数做旋转和相机跟随
要解决的问题:相机要始终看着玩家、旋转平滑;如果直接用欧拉角(三个独立角度)连续转,会撞上"万向节锁",出现相机抽搐。
Godot 4.x 内部就是用四元数(Quaternion)存旋转的,API 也把它暴露给你:
# 从欧拉角创建四元数:绕 Y 轴转 90° var q := Quaternion.from_euler(Vector3(0, PI / 2, 0)) # slerp:两个旋转之间的平滑插值(球面线性插值) var from := Quaternion.IDENTITY var to := q var mid := from.slerp(to, 0.5) # 走到一半的旋转四元数和Basis可以互转,接进Transform3D就能用:
# 四元数 → 3D 变换 var transform := Transform3D(Basis(Quaternion.from_euler(Vector3(0, deg_to_rad(45), 0))), Vector3.ZERO) # 变换 → 四元数(注意:basis 需正交归一) var q_back := transform.basis.get_rotation_quaternion()最小可用的 3D 相机跟随
extends Camera3D @export var target: Node3D @export var offset := Vector3(0, 2, -5) @export var follow_speed := 5.0 func _process(delta: float) -> void: # lerp 让相机平滑地趋近期望位置(向量插值) global_position = global_position.lerp( target.global_position + offset, follow_speed * delta) # look_at 内部用四元数算朝向,避免欧拉角抖动 look_at(target.global_position, Vector3.UP)lerp负责位置过渡,look_at负责朝向,两者配合就是最常见的第三人称跟随相机。Quaternion的完整方法(包括spherical_cubic_interpolate之类)见 classes/class_quaternion.rst。
收尾:性能小贴士、实战路线和速查表
写热点代码时的三个习惯
- 比较距离用平方:只判断"是否进入范围"时,用
position.distance_squared_to(target) < range * range,省掉开方。 - 循环里别反复求逆:
transform.affine_inverse()结果不变的话,先算一次存变量。 - 批量转换用 Basis:一组同向旋转下的向量,用同一个
basis连乘比逐个transform * v更直观也更省。
建议的动手路线
- 用"任务一"的脚本做一个摇杆/键盘控制的 2D 角色,加
lerp_angle转身。 - 加一个敌人:用点积视锥检测玩家,用
Vector2.angle_to平滑转向,触发攻击。 - 搭一个 3D 场景:给相机套上"任务四"的跟随脚本,再把
offset改成可用键盘调整,体会四元数视角。
做完这三个小项目,上表里 80% 的方法你都实际敲过一遍了。
核心方法速查表
| 需求 | 2D 方法 | 3D 方法 | 说明 |
|---|---|---|---|
| 单位化取方向 | v.normalized() | v.normalized() | 移动方向、视锥检测 |
| 距离比较 | a.distance_squared_to(b) | 同左 | 免开方的范围判断 |
| 角度判断(点积) | a.dot(b) | 同左 | 面向检测、投影 |
| 侧向/法线(叉积) | a.cross(b)(标量) | a.cross(b)(Vector3) | 转向方向、表面法线 |
| 位置插值 | a.lerp(b, t) | 同左 | 相机跟随、缓动 |
| 旋转插值 | lerp_angle(a, b, t) | q.slerp(r, t) | 平滑转向 |
| 组合/传递 | Transform2D * t | Transform3D * t | 局部 ↔ 全局换算 |
| 反变换 | t.affine_inverse() | 同左 | 世界坐标转局部坐标 |
参考文档
以上示例均基于 Godot 4.x。官方文档中对应章节:数学教程位于 tutorials/math/(含向量与矩阵专题),各类完整 API 在 classes/ 目录下。建议对照vector_math.rst、matrices_and_transforms.rst、vectors_advanced.rst三篇原文,配合本文的任务路线练习一遍。
【免费下载链接】godot-docsGodot Engine official documentation项目地址: https://gitcode.com/GitHub_Trending/go/godot-docs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考