16. 游戏数学基础
游戏数学基础
你不需要成为数学家才能做游戏,但有一些数学工具是你几乎每天都会用到的。就像厨师不需要懂化学,但必须知道"油温多少度食材会焦"。本章用最直白的方式讲解游戏开发中最常用的数学概念,每个概念都配有实际的代码示例。
本章你将学到
- 向量运算(点乘、叉乘、归一化、投影)
- 矩阵与变换(平移、旋转、缩放)
- 四元数详解
- 插值(Lerp、Slerp、SmoothStep)
- 贝塞尔曲线
- 三角函数在游戏中的应用
- 噪声算法(Perlin/Simplex)
- 概率与随机(权重随机、洗牌算法)
数学概念关系图
向量运算
向量是游戏开发中用得最多的数学概念。简单说,向量就是一个"有方向的箭头"——既有长度又有方向。
归一化(Normalize)
归一化就是把向量的长度变成 1,只保留方向。就像说"朝东走"而不是"朝东走 100 米"。
用途:移动方向、射击方向、力的方向。
using Godot;
public partial class VectorExamples : Node
{
public override void _Ready()
{
// 归一化示例:角色移动
Vector3 velocity = new Vector3(3, 0, 4); // 长度为 5
Vector3 direction = velocity.Normalized(); // 长度变为 1: (0.6, 0, 0.8)
GD.Print($"原始向量: {velocity}, 长度: {velocity.Length()}");
GD.Print($"归一化后: {direction}, 长度: {direction.Length()}");
}
}extends Node
func _ready():
# 归一化示例:角色移动
var velocity = Vector3(3, 0, 4) # 长度为 5
var direction = velocity.normalized() # 长度变为 1: (0.6, 0, 0.8)
print("原始向量: ", velocity, " 长度: ", velocity.length())
print("归一化后: ", direction, " 长度: ", direction.length())点乘(Dot Product)
点乘告诉你两个向量"有多一致"。结果是 -1 到 1 之间的数字:
- 1 = 两个向量方向完全相同
- 0 = 两个向量互相垂直
- -1 = 两个向量方向完全相反
用途:判断敌人是否在面前(视野检测)、计算光照强度、计算夹角。
using Godot;
public partial class DotProductExample : Node3D
{
/// <summary>
/// 视野检测:目标是否在前方 cone 角度内
/// </summary>
public bool IsTargetInFieldOfView(Vector3 forward, Vector3 toTarget, float coneAngleDeg)
{
// 点乘 = cos(夹角)
float dot = forward.Normalized().Dot(toTarget.Normalized());
// 将锥角转为 cos 值
float threshold = Mathf.Cos(Mathf.DegToRad(coneAngleDeg / 2));
return dot >= threshold;
}
public override void _Ready()
{
// 示例:判断敌人是否在 90 度视野内
Vector3 playerForward = -Vector3.UnitZ; // 玩家面朝 -Z 方向
Vector3 toEnemyA = new Vector3(1, 0, -1).Normalized(); // 前方偏右
Vector3 toEnemyB = new Vector3(0, 0, 1).Normalized(); // 正后方
GD.Print($"敌人A在前方90度内?{IsTargetInFieldOfView(playerForward, toEnemyA, 90)}");
// 输出: True(前方偏右,在视野内)
GD.Print($"敌人B在前方90度内?{IsTargetInFieldOfView(playerForward, toEnemyB, 90)}");
// 输出: False(在正后方,不在视野内)
}
}extends Node3D
## 视野检测:目标是否在前方 cone 角度内
func is_target_in_field_of_view(forward: Vector3, to_target: Vector3, cone_angle_deg: float) -> bool:
# 点乘 = cos(夹角)
var dot = forward.normalized().dot(to_target.normalized())
# 将锥角转为 cos 值
var threshold = cos(deg_to_rad(cone_angle_deg / 2))
return dot >= threshold
func _ready():
# 示例:判断敌人是否在 90 度视野内
var player_forward = Vector3.FORWARD # 玩家面朝 -Z 方向
var to_enemy_a = Vector3(1, 0, -1).normalized() # 前方偏右
var to_enemy_b = Vector3(0, 0, 1).normalized() # 正后方
print("敌人A在前方90度内?", is_target_in_field_of_view(player_forward, to_enemy_a, 90))
# 输出: True(前方偏右,在视野内)
print("敌人B在前方90度内?", is_target_in_field_of_view(player_forward, to_enemy_b, 90))
# 输出: False(在正后方,不在视野内)叉乘(Cross Product)
叉乘的结果是一个新向量,垂直于原来的两个向量。右手定则:如果右手的食指指向向量 A,中指指向向量 B,则拇指指向叉乘结果的方向。
用途:计算面的法线(用于光照和碰撞)、判断物体在左侧还是右侧。
using Godot;
public partial class CrossProductExample : Node3D
{
/// <summary>
/// 判断目标在左侧还是右侧
/// </summary>
public string GetSide(Vector3 forward, Vector3 up, Vector3 toTarget)
{
Vector3 right = forward.Cross(up).Normalized();
float dot = right.Dot(toTarget.Normalized());
if (dot > 0.1f) return "右侧";
if (dot < -0.1f) return "左侧";
return "正前方或正后方";
}
/// <summary>
/// 计算地面法线(用于让角色贴合斜面)
/// </summary>
public Vector3 GetGroundNormal(Vector3 surfacePointA, Vector3 surfacePointB, Vector3 surfacePointC)
{
Vector3 ab = surfacePointB - surfacePointA;
Vector3 ac = surfacePointC - surfacePointA;
return ab.Cross(ac).Normalized();
}
public override void _Ready()
{
Vector3 forward = -Vector3.UnitZ;
Vector3 up = Vector3.UnitY;
Vector3 toTarget = new Vector3(2, 0, -1);
GD.Print($"目标在: {GetSide(forward, up, toTarget)}");
// 输出: 右侧
}
}extends Node3D
## 判断目标在左侧还是右侧
func get_side(forward: Vector3, up: Vector3, to_target: Vector3) -> String:
var right = forward.cross(up).normalized()
var dot = right.dot(to_target.normalized())
if dot > 0.1:
return "右侧"
elif dot < -0.1:
return "左侧"
else:
return "正前方或正后方"
## 计算地面法线(用于让角色贴合斜面)
func get_ground_normal(a: Vector3, b: Vector3, c: Vector3) -> Vector3:
var ab = b - a
var ac = c - a
return ab.cross(ac).normalized()
func _ready():
var forward = Vector3.FORWARD
var up = Vector3.UP
var to_target = Vector3(2, 0, -1)
print("目标在: ", get_side(forward, up, to_target))
# 输出: 右侧矩阵与变换
游戏中的每个物体都有位置、旋转、缩放——这三者合起来就是一个"变换"(Transform)。Godot 用 Transform3D 来表示。
Transform3D 的组成
Transform3D = Basis(旋转和缩放) + Origin(位置)
Basis 是一个 3x3 矩阵:
| X轴方向 | Y轴方向 | Z轴方向 |
|---------|---------|---------|
| Xx Xy Xz | Yx Yy Yz | Zx Zy Zz |
Origin 是位置向量:(x, y, z)using Godot;
public partial class TransformExamples : Node3D
{
public override void _Ready()
{
// 1. 平移(移动位置)
Transform3D t = Transform;
t.Origin += new Vector3(5, 0, 0); // 向右移动 5 单位
Transform = t;
// 2. 旋转(绕 Y 轴旋转 45 度)
RotateY(Mathf.DegToRad(45));
// 3. 缩放(放大 2 倍)
Scale = new Vector3(2, 2, 2);
// 4. 看向目标
Vector3 target = new Vector3(10, 0, 5);
LookAt(target, Vector3.Up);
// 5. 组合变换:先旋转再平移
Transform3D rotation = Transform3D.Identity.Rotated(Vector3.Up, Mathf.DegToRad(90));
Transform3D translation = Transform3D.Identity.Translated(new Vector3(5, 0, 0));
Transform3D combined = translation * rotation; // 注意顺序:先旋转后平移
}
}extends Node3D
func _ready():
# 1. 平移(移动位置)
position += Vector3(5, 0, 0) # 向右移动 5 单位
# 2. 旋转(绕 Y 轴旋转 45 度)
rotate_y(deg_to_rad(45))
# 3. 缩放(放大 2 倍)
scale = Vector3(2, 2, 2)
# 4. 看向目标
var target = Vector3(10, 0, 5)
look_at(target, Vector3.UP)
# 5. 组合变换:先旋转再平移
var rotation = Transform3D.IDENTITY.rotated(Vector3.UP, deg_to_rad(90))
var translation = Transform3D.IDENTITY.translated(Vector3(5, 0, 0))
var combined = translation * rotation # 注意顺序:先旋转后平移变换的顺序很重要
矩阵乘法的顺序会影响结果。A * B 不等于 B * A。"先旋转再平移"和"先平移再旋转"的最终位置完全不同。记住:从右往左读,右边的变换先执行。
四元数详解
为什么不用欧拉角?
欧拉角(X/Y/Z 三个角度)有一个经典问题叫万向锁(Gimbal Lock):当某个轴旋转到 90 度时,另外两个轴会"重叠",导致丢失一个自由度。想象你抬头看天花板时,转头和左右歪头的区别变得模糊了。
四元数的优势
四元数用 4 个数字 (x, y, z, w) 表示旋转,避免了万向锁,还能在两个旋转之间做平滑插值。
using Godot;
public partial class QuaternionExamples : Node3D
{
[Export] public float RotationSpeed { get; set; } = 3.0f;
[Export] public Node3D Target { get; set; }
public override void _Process(double delta)
{
if (Target == null) return;
// 1. 从当前朝向平滑旋转到目标方向
Vector3 direction = (Target.GlobalPosition - GlobalPosition).Normalized();
Basis targetBasis = Basis.LookingAt(direction, Vector3.Up);
Quaternion targetQuat = targetBasis.GetRotationQuaternion();
Quaternion currentQuat = Transform.Basis.GetRotationQuaternion();
// Slerp(球面线性插值):平滑旋转
Quaternion smoothed = currentQuat.Slerp(targetQuat, (float)delta * RotationSpeed);
Transform3D newTransform = Transform;
newTransform.Basis = new Basis(smoothed);
Transform = newTransform;
// 2. 使用四元数避免万向锁的 FPS 摄像机旋转
// (在第一人称控制器中,X轴旋转不超过 +-90度就不会锁)
}
/// <summary>
/// 在两个旋转之间做平滑过渡
/// </summary>
public void RotateTowards(Vector3 targetPosition, float speed)
{
Vector3 direction = (targetPosition - GlobalPosition).Normalized();
Quaternion from = Quaternion.Identity;
Quaternion to = Basis.LookingAt(direction, Vector3.Up)
.GetRotationQuaternion();
// Slerp 参数 0~1 表示过渡进度
Quaternion result = from.Slerp(to, speed);
Rotation = result.GetEuler();
}
}extends Node3D
@export var rotation_speed: float = 3.0
@export var target: Node3D
func _process(delta):
if target == null:
return
# 1. 从当前朝向平滑旋转到目标方向
var direction = (target.global_position - global_position).normalized()
var target_basis = Basis.looking_at(direction, Vector3.UP)
var target_quat = target_basis.get_rotation_quaternion()
var current_quat = transform.basis.get_rotation_quaternion()
# Slerp(球面线性插值):平滑旋转
var smoothed = current_quat.slerp(target_quat, delta * rotation_speed)
var new_transform = transform
new_transform.basis = Basis(smoothed)
transform = new_transform
## 在两个旋转之间做平滑过渡
func rotate_towards(target_position: Vector3, speed: float):
var direction = (target_position - global_position).normalized()
var from = Quaternion.IDENTITY
var to = Basis.looking_at(direction, Vector3.UP).get_rotation_quaternion()
# Slerp 参数 0~1 表示过渡进度
var result = from.slerp(to, speed)
rotation = result.get_euler()插值(Lerp、Slerp、SmoothStep)
插值就是"在两个值之间逐渐过渡"。这是游戏中最常用的数学操作之一。
| 插值类型 | 效果 | 适用场景 |
|---|---|---|
| Lerp | 线性(匀速)过渡 | 位置、颜色、数值的平滑变化 |
| Slerp | 球面(沿弧线)过渡 | 旋转的平滑变化 |
| SmoothStep | 先慢后快再慢 | 相机移动、UI 动画 |
using Godot;
public partial class InterpolationExamples : Node3D
{
private Vector3 _startPos;
private Vector3 _endPos;
private float _progress;
public override void _Ready()
{
_startPos = GlobalPosition;
_endPos = GlobalPosition + new Vector3(10, 0, 0);
_progress = 0;
}
public override void _Process(double delta)
{
// ========== Lerp 示例 ==========
// 在 startPos 和 endPos 之间线性插值
// t 的范围是 0~1(0=起点,1=终点)
_progress = Mathf.Min(_progress + (float)delta * 0.5f, 1.0f);
GlobalPosition = _startPos.Lerp(_endPos, _progress);
// ========== SmoothStep 示例 ==========
// 使用 SmoothStep 让运动更自然(先慢后快再慢)
float smoothProgress = Mathf.SmoothStep(0.0f, 1.0f, _progress);
Vector3 smoothPos = _startPos.Lerp(_endPos, smoothProgress);
// ========== 数值 Lerp 示例 ==========
// 平滑过渡生命值条
float displayHp = Mathf.Lerp(80.0f, 100.0f, (float)delta * 5.0f);
// ========== 颜色 Lerp 示例 ==========
// 从红色渐变到绿色
Color fromColor = Colors.Red;
Color toColor = Colors.Green;
Color currentColor = fromColor.Lerp(toColor, _progress);
}
}extends Node3D
var start_pos: Vector3
var end_pos: Vector3
var progress: float = 0.0
func _ready():
start_pos = global_position
end_pos = global_position + Vector3(10, 0, 0)
func _process(delta):
# ========== Lerp 示例 ==========
# 在 start_pos 和 end_pos 之间线性插值
# t 的范围是 0~1(0=起点,1=终点)
progress = minf(progress + delta * 0.5, 1.0)
global_position = start_pos.lerp(end_pos, progress)
# ========== SmoothStep 示例 ==========
# 使用 SmoothStep 让运动更自然(先慢后快再慢)
var smooth_progress = smoothstep(0.0, 1.0, progress)
var smooth_pos = start_pos.lerp(end_pos, smooth_progress)
# ========== 数值 Lerp 示例 ==========
# 平滑过渡生命值条
var display_hp = lerpf(80.0, 100.0, delta * 5.0)
# ========== 颜色 Lerp 示例 ==========
# 从红色渐变到绿色
var from_color = Color.RED
var to_color = Color.GREEN
var current_color = from_color.lerp(to_color, progress)Lerp 的两种用法
- 动画式:
progress从 0 递增到 1,value = lerp(a, b, progress)——用于一次性的过渡动画 - 追赶式:
value = lerp(value, target, delta * speed)——用于持续追踪(如摄像机跟随)。这种写法每帧都把当前值往目标值拉近一点,形成"弹性"效果
贝塞尔曲线
贝塞尔曲线是通过几个"控制点"画出的平滑曲线。想象你拉着一根橡皮筋,中间加了几个钉子——橡皮筋会沿着钉子形成一条优美的弧线。
用途:角色移动路径、摄像机轨道、UI 动画曲线、弹道轨迹。
using Godot;
public partial class BezierCurve : Node3D
{
[Export] public Vector3 P0 { get; set; } = Vector3.Zero; // 起点
[Export] public Vector3 P1 { get; set; } = new(2, 5, 0); // 控制点1
[Export] public Vector3 P2 { get; set; } = new(8, 5, 0); // 控制点2
[Export] public Vector3 P3 { get; set; } = new(10, 0, 0); // 终点
/// <summary>
/// 三次贝塞尔曲线(4 个控制点)
/// </summary>
public Vector3 GetPoint(float t)
{
// t 范围 0~1
float u = 1.0f - t;
float tt = t * t;
float uu = u * u;
float uuu = uu * u;
float ttt = tt * t;
Vector3 point = uuu * P0; // (1-t)^3 * P0
point += 3 * uu * t * P1; // 3(1-t)^2 * t * P1
point += 3 * u * tt * P2; // 3(1-t) * t^2 * P2
point += ttt * P3; // t^3 * P3
return point;
}
/// <summary>
/// 二次贝塞尔曲线(3 个控制点)
/// </summary>
public Vector3 GetQuadraticPoint(Vector3 start, Vector3 control, Vector3 end, float t)
{
float u = 1.0f - t;
return u * u * start + 2 * u * t * control + t * t * end;
}
// 使用示例:让物体沿贝塞尔曲线移动
private float _t;
public override void _Process(double delta)
{
_t += (float)delta * 0.3f;
if (_t > 1.0f) _t = 0.0f;
GlobalPosition = GetPoint(_t);
}
}extends Node3D
@export var p0: Vector3 = Vector3.ZERO # 起点
@export var p1: Vector3 = Vector3(2, 5, 0) # 控制点1
@export var p2: Vector3 = Vector3(8, 5, 0) # 控制点2
@export var p3: Vector3 = Vector3(10, 0, 0) # 终点
## 三次贝塞尔曲线(4 个控制点)
func get_point(t: float) -> Vector3:
# t 范围 0~1
var u = 1.0 - t
var tt = t * t
var uu = u * u
var uuu = uu * u
var ttt = tt * t
var point = uuu * p0 # (1-t)^3 * P0
point += 3 * uu * t * p1 # 3(1-t)^2 * t * P1
point += 3 * u * tt * p2 # 3(1-t) * t^2 * P2
point += ttt * p3 # t^3 * P3
return point
## 二次贝塞尔曲线(3 个控制点)
func get_quadratic_point(start: Vector3, control: Vector3, end: Vector3, t: float) -> Vector3:
var u = 1.0 - t
return u * u * start + 2 * u * t * control + t * t * end
# 使用示例:让物体沿贝塞尔曲线移动
var _t: float = 0.0
func _process(delta):
_t += delta * 0.3
if _t > 1.0:
_t = 0.0
global_position = get_point(_t)三角函数在游戏中的应用
正弦和余弦不只是课本上的公式,它们在游戏里无处不在:
| 用途 | 使用的函数 | 示例 |
|---|---|---|
| 上下浮动 | sin(time) | 悬浮的宝箱、上下弹跳的敌人 |
| 圆形运动 | cos/sin | 围绕某个点旋转的物体 |
| 波浪效果 | sin(x + time) | 水面波浪、旗帜飘动 |
| 正弦瞄准 | sin + cos | 子弹扇形散射 |
using Godot;
public partial class TrigExamples : Node3D
{
private float _time;
[Export] public float FloatAmplitude { get; set; } = 0.5f;
[Export] public float FloatSpeed { get; set; } = 2.0f;
public override void _Process(double delta)
{
_time += (float)delta;
// 1. 上下浮动效果(宝箱、道具)
float yOffset = Mathf.Sin(_time * FloatSpeed) * FloatAmplitude;
Position = new Vector3(Position.X, yOffset, Position.Z);
// 2. 圆形运动(围绕中心旋转)
float radius = 5.0f;
float circleX = Mathf.Cos(_time) * radius;
float circleZ = Mathf.Sin(_time) * radius;
// GlobalPosition = center + new Vector3(circleX, 0, circleZ);
// 3. 子弹扇形散射
int bulletCount = 5;
float spreadAngle = 30.0f; // 总散射角度
for (int i = 0; i < bulletCount; i++)
{
float angle = Mathf.DegToRad(
-spreadAngle / 2 + spreadAngle * i / (bulletCount - 1));
Vector3 direction = new Vector3(
Mathf.Sin(angle),
0,
-Mathf.Cos(angle)
);
// SpawnBullet(direction);
GD.Print($"子弹 {i} 方向: {direction}");
}
}
}extends Node3D
var _time: float = 0.0
@export var float_amplitude: float = 0.5
@export var float_speed: float = 2.0
func _process(delta):
_time += delta
# 1. 上下浮动效果(宝箱、道具)
var y_offset = sin(_time * float_speed) * float_amplitude
position = Vector3(position.x, y_offset, position.z)
# 2. 圆形运动(围绕中心旋转)
var radius = 5.0
var circle_x = cos(_time) * radius
var circle_z = sin(_time) * radius
# global_position = center + Vector3(circle_x, 0, circle_z)
# 3. 子弹扇形散射
var bullet_count = 5
var spread_angle = 30.0 # 总散射角度
for i in range(bullet_count):
var angle = deg_to_rad(
-spread_angle / 2 + spread_angle * i / (bullet_count - 1))
var direction = Vector3(
sin(angle),
0,
-cos(angle)
)
# spawn_bullet(direction)
print("子弹 %d 方向: %s" % [i, direction])噪声算法(Perlin/Simplex)
噪声不是"随机数",而是一种"看起来随机但连续变化"的函数。就像山脉的轮廓——不是完全随机的锯齿,而是有高低起伏但过渡自然的曲线。
用途:地形高度图、云朵生成、纹理变化、洞穴系统。
using Godot;
public partial class NoiseExample : Node3D
{
[Export] public FastNoiseLite Noise { get; set; }
[Export] public int TerrainSize { get; set; } = 50;
[Export] public float HeightScale { get; set; } = 10.0f;
public override void _Ready()
{
if (Noise == null)
{
Noise = new FastNoiseLite();
Noise.NoiseType = FastNoiseLite.NoiseTypeEnum.Simplex;
Noise.Seed = 12345;
Noise.Frequency = 0.05f; // 值越小,地形越平缓
}
GenerateTerrain();
}
private void GenerateTerrain()
{
for (int x = 0; x < TerrainSize; x++)
{
for (int z = 0; z < TerrainSize; z++)
{
// 获取噪声值(-1 到 1)
float noiseVal = Noise.GetNoise2D(x, z);
// 转为高度(0 到 HeightScale)
float height = (noiseVal + 1.0f) / 2.0f * HeightScale;
// 在此处创建地形方块
// var block = new MeshInstance3D();
// block.Position = new Vector3(x, height, z);
// AddChild(block);
}
}
GD.Print("地形生成完成");
}
/// <summary>
/// 多层噪声叠加(更自然的地形)
/// </summary>
private float GetMultiOctaveNoise(float x, float z, int octaves)
{
float value = 0;
float amplitude = 1.0f;
float frequency = 1.0f;
float maxValue = 0;
for (int i = 0; i < octaves; i++)
{
value += Noise.GetNoise2D(x * frequency, z * frequency) * amplitude;
maxValue += amplitude;
amplitude *= 0.5f; // 每层振幅减半
frequency *= 2.0f; // 每层频率翻倍
}
return value / maxValue; // 归一化到 -1~1
}
}extends Node3D
@export var noise: FastNoiseLite
@export var terrain_size: int = 50
@export var height_scale: float = 10.0
func _ready():
if noise == null:
noise = FastNoiseLite.new()
noise.noise_type = FastNoiseLite.NoiseType.TYPE_SIMPLEX
noise.seed = 12345
noise.frequency = 0.05 # 值越小,地形越平缓
_generate_terrain()
func _generate_terrain():
for x in range(terrain_size):
for z in range(terrain_size):
# 获取噪声值(-1 到 1)
var noise_val = noise.get_noise_2d(x, z)
# 转为高度(0 到 height_scale)
var height = (noise_val + 1.0) / 2.0 * height_scale
# 在此处创建地形方块
# var block = MeshInstance3D.new()
# block.position = Vector3(x, height, z)
# add_child(block)
pass
print("地形生成完成")
## 多层噪声叠加(更自然的地形)
func _get_multi_octave_noise(x: float, z: float, octaves: int) -> float:
var value = 0.0
var amplitude = 1.0
var frequency = 1.0
var max_value = 0.0
for i in range(octaves):
value += noise.get_noise_2d(x * frequency, z * frequency) * amplitude
max_value += amplitude
amplitude *= 0.5 # 每层振幅减半
frequency *= 2.0 # 每层频率翻倍
return value / max_value # 归一化到 -1~1概率与随机
权重随机(掉落系统)
不同物品有不同的掉落概率——传说装备 1%、稀有装备 10%、普通物品 50%、什么都没有 39%。
using Godot;
using System.Collections.Generic;
public class WeightedRandom<T>
{
private List<(T item, float weight)> _items = new();
private float _totalWeight;
public void Add(T item, float weight)
{
_items.Add((item, weight));
_totalWeight += weight;
}
public T Roll()
{
float roll = (float)GD.RandRange(0.0, _totalWeight);
float cumulative = 0;
foreach (var (item, weight) in _items)
{
cumulative += weight;
if (roll <= cumulative)
return item;
}
return default;
}
}
// 使用示例
public partial class LootSystem : Node
{
public override void _Ready()
{
var lootTable = new WeightedRandom<string>();
lootTable.Add("legendary_sword", 1.0f); // 1% 权重
lootTable.Add("rare_armor", 10.0f); // 10% 权重
lootTable.Add("common_potion", 50.0f); // 50% 权重
lootTable.Add("nothing", 39.0f); // 39% 权重
// 模拟 10 次掉落
for (int i = 0; i < 10; i++)
{
string result = lootTable.Roll();
GD.Print($"掉落 #{i + 1}: {result}");
}
}
}extends Node
## 权重随机实现
class WeightedRandom:
var _items: Array = [] # [{item, weight}]
var _total_weight: float = 0.0
func add(item, weight: float):
_items.append({"item": item, "weight": weight})
_total_weight += weight
func roll():
var roll_val = randf() * _total_weight
var cumulative = 0.0
for entry in _items:
cumulative += entry.weight
if roll_val <= cumulative:
return entry.item
return null
# 使用示例
func _ready():
var loot_table = WeightedRandom.new()
loot_table.add("legendary_sword", 1.0) # 1% 权重
loot_table.add("rare_armor", 10.0) # 10% 权重
loot_table.add("common_potion", 50.0) # 50% 权重
loot_table.add("nothing", 39.0) # 39% 权重
# 模拟 10 次掉落
for i in range(10):
var result = loot_table.roll()
print("掉落 #%d: %s" % [i + 1, result])洗牌算法(Fisher-Yates Shuffle)
当你需要随机打乱一个数组(比如洗牌、随机关卡顺序)时,Fisher-Yates 是最高效且公平的算法。
using Godot;
using System;
public static class ShuffleUtil
{
/// <summary>
/// Fisher-Yates 洗牌算法
/// </summary>
public static void Shuffle<T>(T[] array)
{
var rng = new Random();
for (int i = array.Length - 1; i > 0; i--)
{
int j = rng.Next(i + 1);
// 交换 i 和 j
(array[i], array[j]) = (array[j], array[i]);
}
}
}## Fisher-Yates 洗牌算法
func shuffle_array(arr: Array) -> Array:
var result = arr.duplicate()
for i in range(result.size() - 1, 0, -1):
var j = randi_range(0, i)
# 交换 i 和 j
var temp = result[i]
result[i] = result[j]
result[j] = temp
return result本章小结
| 数学概念 | 一句话总结 | 最常用场景 |
|---|---|---|
| 向量归一化 | 只保留方向,丢掉长度 | 移动方向、力的方向 |
| 点乘 | 两个方向有多一致 | 视野检测、角度计算 |
| 叉乘 | 得到垂直于两个方向的向量 | 法线计算、左右判断 |
| Transform3D | 位置 + 旋转 + 缩放 | 所有物体的变换 |
| 四元数 | 没有万向锁的旋转表示 | 平滑旋转、摄像机 |
| Lerp | 线性过渡 | 所有平滑变化 |
| Slerp | 弧线过渡 | 旋转插值 |
| 贝塞尔曲线 | 控制点决定的平滑曲线 | 路径、轨迹 |
| 三角函数 | sin/cos 波浪和圆 | 浮动效果、圆形运动 |
| 噪声 | 看似随机但连续的值 | 地形、纹理 |
| 权重随机 | 不同概率的抽奖 | 掉落系统 |
不需要记住所有公式——Godot 已经帮你实现了大部分数学运算。重要的是理解每个概念"做什么用",需要时查阅本章即可。
相关章节
- 基础篇 - 3D 空间与变换:3D 坐标系基础
- 程序化生成:噪声算法的深入应用
- 高级动画:插值在动画中的应用
