9. 打磨与发布
2026/4/14大约 8 分钟
打磨与发布
功能都做完了?恭喜!但一个"能跑"的游戏和一个"好玩"的游戏之间,差的恰恰就是最后这步——打磨。就像买新车,引擎装好了还得调试、清洗、打蜡,才能交到客户手上。
本章你将学到
- 操控手感调优(灵敏度、死区)
- 多赛道多车型管理
- 多平台性能适配
- 赛事模式设计(单人/分屏多人)
- 回放系统基础
操控手感调优
操控手感是竞速游戏成败的关键。手感不对,再好的画面和音效都没用。
什么是"好手感"?
好手感就是:玩家的意图和摩托车的反应之间零延迟。玩家想左转,摩托车立刻左转;想刹车,立刻减速。但"零延迟"不等于"瞬间响应"——那样反而会让人觉得太灵敏、不好控制。
关键调优参数
| 参数 | 作用 | 调优建议 |
|---|---|---|
| 转向灵敏度 | 方向盘/摇杆移动多少,摩托车转多少 | 太灵敏→难控制;太迟钝→反应慢 |
| 输入死区 | 摇杆在什么范围内视为"没动" | 推荐 0.1~0.15,消除摇杆漂移 |
| 加速曲线 | 油门输入到实际加速的映射 | 用曲线而非线性,让微操更精确 |
| 刹车曲线 | 刹车输入到实际减速的映射 | 前半段温和、后半段急 |
| 转向辅助 | 低速时自动增加转向力 | 帮助新手在低速时也能灵活转向 |
输入处理代码
C#
// 输入处理器 - 处理死区和灵敏度
public partial class InputProcessor : Node
{
[Export] public float DeadZone = 0.12f; // 死区
[Export] public float SteerSensitivity = 1.5f; // 转向灵敏度
[Export] public float ThrottleCurve = 2.0f; // 油门曲线指数
// 处理带死区的摇杆输入
public float ApplyDeadZone(float rawInput)
{
if (Mathf.Abs(rawInput) < DeadZone)
return 0f;
// 重新映射,让死区后的输入从0开始
float sign = Mathf.Sign(rawInput);
float magnitude = (Mathf.Abs(rawInput) - DeadZone) / (1f - DeadZone);
return sign * magnitude;
}
// 应用灵敏度
public float ApplySensitivity(float input, float sensitivity)
{
return Mathf.Clamp(input * sensitivity, -1f, 1f);
}
// 应用非线性曲线(让微操更精确)
public float ApplyCurve(float input, float curvePower)
{
return Mathf.Sign(input) * Mathf.Pow(Mathf.Abs(input), curvePower);
}
// 获取处理后的转向输入
public float GetSteerInput()
{
float raw = Input.GetAxis("steer_left", "steer_right");
float noDeadZone = ApplyDeadZone(raw);
float curved = ApplyCurve(noDeadZone, 1.5f);
return ApplySensitivity(curved, SteerSensitivity);
}
// 获取处理后的油门输入
public float GetThrottleInput()
{
float raw = Input.GetStrength("accelerate");
return ApplyCurve(raw, ThrottleCurve);
}
}GDScript
# 输入处理器 - 处理死区和灵敏度
extends Node
@export var dead_zone: float = 0.12 # 死区
@export var steer_sensitivity: float = 1.5 # 转向灵敏度
@export var throttle_curve: float = 2.0 # 油门曲线指数
# 处理带死区的摇杆输入
func apply_dead_zone(raw_input: float) -> float:
if absf(raw_input) < dead_zone:
return 0.0
# 重新映射
var sign = signf(raw_input)
var magnitude = (absf(raw_input) - dead_zone) / (1.0 - dead_zone)
return sign * magnitude
# 应用灵敏度
func apply_sensitivity(input: float, sensitivity: float) -> float:
return clampf(input * sensitivity, -1.0, 1.0)
# 应用非线性曲线
func apply_curve(input: float, curve_power: float) -> float:
return signf(input) * pow(absf(input), curve_power)
# 获取处理后的转向输入
func get_steer_input() -> float:
var raw = Input.get_axis("steer_left", "steer_right")
var no_dead_zone = apply_dead_zone(raw)
var curved = apply_curve(no_dead_zone, 1.5)
return apply_sensitivity(curved, steer_sensitivity)
# 获取处理后的油门输入
func get_throttle_input() -> float:
var raw = Input.get_strength("accelerate")
return apply_curve(raw, throttle_curve)多赛道多车型管理
游戏不能只有一条赛道、一辆车。我们需要一个管理系统来方便地切换不同的赛道和车型。
数据驱动设计
把赛道和车型的参数做成数据文件(Resource),而不是硬编码在代码里。这样添加新内容不需要改代码:
C#
// 车型数据
[GlobalClass]
public partial class VehicleData : Resource
{
[Export] public string VehicleName = "越野摩托车";
[Export] public string ModelPath; // 模型场景路径
[Export] public float MaxSpeed = 50f; // 最高速度 m/s
[Export] public float Acceleration = 800f; // 加速力
[Export] public float Mass = 200f; // 质量 kg
[Export] public float Handling = 1.0f; // 操控性(0.5~1.5)
[Export] public string Description = "均衡型:适合各种路况";
}
// 赛道选择管理器
public partial class TrackSelectManager : Node
{
[Export] public Godot.Collections.Array<PackedScene> TrackScenes = new();
[Export] public Godot.Collections.Array<VehicleData> Vehicles = new();
private int _selectedTrack;
private int _selectedVehicle;
public void SelectTrack(int index)
{
_selectedTrack = Mathf.Clamp(index, 0, TrackScenes.Count - 1);
GD.Print($"选择赛道:{_selectedTrack}");
}
public void SelectVehicle(int index)
{
_selectedVehicle = Mathf.Clamp(index, 0, Vehicles.Count - 1);
GD.Print($"选择车辆:{Vehicles[_selectedVehicle].VehicleName}");
}
public VehicleData GetCurrentVehicle() => Vehicles[_selectedVehicle];
public void StartRace()
{
// 加载赛道场景
GetTree().ChangeSceneToPacked(TrackScenes[_selectedTrack]);
}
}GDScript
# 车型数据
class_name VehicleData
extends Resource
@export var vehicle_name: String = "越野摩托车"
@export var model_path: String # 模型场景路径
@export var max_speed: float = 50.0 # 最高速度 m/s
@export var acceleration: float = 800.0 # 加速力
@export var mass: float = 200.0 # 质量 kg
@export var handling: float = 1.0 # 操控性(0.5~1.5)
@export var description: String = "均衡型:适合各种路况"
# 赛道选择管理器
extends Node
@export var track_scenes: Array[PackedScene] = []
@export var vehicles: Array[VehicleData] = []
var _selected_track: int = 0
var _selected_vehicle: int = 0
func select_track(index: int):
_selected_track = clampi(index, 0, track_scenes.size() - 1)
print("选择赛道:%d" % _selected_track)
func select_vehicle(index: int):
_selected_vehicle = clampi(index, 0, vehicles.size() - 1)
print("选择车辆:%s" % vehicles[_selected_vehicle].vehicle_name)
func get_current_vehicle() -> VehicleData:
return vehicles[_selected_vehicle]
func start_race():
get_tree().change_scene_to_packed(track_scenes[_selected_track])多平台性能适配
游戏要在不同设备上流畅运行,需要做一些性能适配。
性能等级
平台导出注意事项
| 平台 | 注意事项 |
|---|---|
| Windows | 最简单,直接导出 .exe |
| macOS | 需要签名和公证,否则被 Gatekeeper 拦截 |
| Linux | 导出 AppImage 或 Flatpak |
| Android | 需要适配触摸屏操控,注意 APK 大小 |
| iOS | 需要 Apple 开发者账号,签名后才能安装 |
| Web | 导出 HTML5,注意浏览器兼容性和 WASM 大小 |
赛事模式设计
单人锦标赛
玩家依次参加多个赛段的比赛,每段累加时间,最终排名:
赛段1: 沙漠之路 → 记录时间
赛段2: 密林穿越 → 累加时间
赛段3: 雪山之巅 → 累加时间
赛段4: 盘山公路 → 累加时间
赛段5: 最终冲刺 → 总成绩排名分屏多人
分屏多人需要在同一个屏幕上显示两个视角。Godot 支持使用多个 Viewport 来实现:
C#
// 分屏设置
public partial class SplitScreenManager : Node
{
[Export] public SubViewportContainer ViewportContainer;
[Export] public Camera3D Player1Camera;
[Export] public Camera3D Player2Camera;
public void SetupSplitScreen(bool horizontal)
{
// 水平分屏(左右)或垂直分屏(上下)
if (horizontal)
{
// 玩家1:左半屏
Player1Camera.GetViewport().Size = new Vector2I(
(int)ViewportContainer.Size.X / 2,
(int)ViewportContainer.Size.Y
);
// 玩家2:右半屏
Player2Camera.GetViewport().Size = new Vector2I(
(int)ViewportContainer.Size.X / 2,
(int)ViewportContainer.Size.Y
);
}
}
}GDScript
# 分屏设置
extends Node
@export var viewport_container: SubViewportContainer
@export var player1_camera: Camera3D
@export var player2_camera: Camera3D
func setup_split_screen(horizontal: bool):
if horizontal:
# 玩家1:左半屏
player1_camera.get_viewport().size = Vector2i(
int(viewport_container.size.x / 2),
int(viewport_container.size.y)
)
# 玩家2:右半屏
player2_camera.get_viewport().size = Vector2i(
int(viewport_container.size.x / 2),
int(viewport_container.size.y)
)回放系统基础
回放系统让玩家可以回看自己的比赛过程。核心思路是录制每帧的关键数据,然后回放时还原。
C#
// 回放录制器
public partial class ReplayRecorder : Node
{
// 每帧记录的数据
public struct FrameData
{
public Vector3 Position;
public Quaternion Rotation;
public float Speed;
public float Timestamp;
}
private readonly List<FrameData> _frames = new();
private RigidBody3D _motorcycle;
private float _recordInterval = 1f / 30f; // 每秒记录30帧
private float _timer;
public override void _Ready()
{
_motorcycle = GetParent<RigidBody3D>();
}
public override void _Process(double delta)
{
_timer += (float)delta;
if (_timer >= _recordInterval)
{
_timer = 0f;
RecordFrame();
}
}
private void RecordFrame()
{
_frames.Add(new FrameData
{
Position = _motorcycle.GlobalPosition,
Rotation = _motorcycle.GlobalTransform.Basis.GetRotationQuaternion(),
Speed = _motorcycle.LinearVelocity.Length(),
Timestamp = (float)Time.GetTicksMsec() / 1000f
});
}
// 获取录制数据
public List<FrameData> GetReplayData() => _frames;
}GDScript
# 每帧记录的数据
class FrameData:
var position: Vector3
var rotation: Quaternion
var speed: float
var timestamp: float
# 回放录制器
extends Node
var _frames: Array[FrameData] = []
var _motorcycle: RigidBody3D
var _record_interval: float = 1.0 / 30.0 # 每秒记录30帧
var _timer: float = 0.0
func _ready():
_motorcycle = get_parent()
func _process(delta):
_timer += delta
if _timer >= _record_interval:
_timer = 0.0
_record_frame()
func _record_frame():
var frame = FrameData.new()
frame.position = _motorcycle.global_position
frame.rotation = _motorcycle.global_transform.basis.get_rotation_quaternion()
frame.speed = _motorcycle.linear_velocity.length()
frame.timestamp = Time.get_ticks_msec() / 1000.0
_frames.append(frame)
# 获取录制数据
func get_replay_data() -> Array[FrameData]:
return _frames发布检查清单
发布前确保以下所有项目都通过:
发布步骤
- 最终测试:找几个朋友玩一下,收集反馈
- 优化构建:使用 Release 模式导出,减小包体积
- 编写说明:简单的操作说明和已知问题列表
- 打包分发:上传到 itch.io / Steam / 其他平台
常见问题
Q:游戏在低端设备上卡怎么办?
首先降低物理帧率到60,然后关闭粒子和阴影。如果还是卡,降低渲染分辨率。Godot 提供了 viewport 的缩放功能,可以在不改变UI大小的情况下降低3D渲染分辨率。
Q:怎么做游戏的设置菜单?
用 Godot 的 ConfigFile 类来保存和读取设置。玩家修改设置时写入配置文件,游戏启动时读取并应用。
Q:怎么控制游戏包大小?
- 压缩纹理(使用 WebP 格式代替 PNG)
- 音频使用 OGG 而不是 WAV
- 移除未使用的资源
- 启用导出时的资源压缩
恭喜完成!
到这里,你已经完成了摩托车拉力锦标赛游戏的全部开发教程。从核心玩法设计到最终的打磨发布,每一步都亲手实现了。希望你在这个过程中学到了东西,更希望你能做出一款让自己和朋友都觉得好玩的竞速游戏!
继续加油,享受游戏开发的乐趣!
