10. 打磨与发布
2026/4/13大约 12 分钟
打磨与发布
从"能玩"到"好玩"
你有没有吃过一道菜,食材都是对的、做法也是对的,但就是觉得"差点什么"?可能是盐放少了,可能是火候不够,可能是摆盘不好看。做游戏也一样——核心玩法做好了,但如果没有"打磨",玩家会觉得粗糙、不专业。
打磨(Polish)就是给游戏加"调料"的过程:让角色移动更流畅、让打击感更强、让画面更漂亮、让音效更带感。这些细节单独看都不起眼,但加在一起就能让游戏从"能玩"变成"好玩"。
性能优化
大量敌人的性能问题
割草游戏的性能瓶颈通常是大量敌人同时存在。当屏幕上有200+个敌人时,以下操作会变得很慢:
| 操作 | 问题 | 解决方案 |
|---|---|---|
| 每帧遍历所有敌人 | O(n) 复杂度,n越大越慢 | 空间分区(四叉树) |
| 每个敌人独立物理计算 | 200个物理体同时计算 | 简化物理(仅用Area检测) |
| 每个敌人独立渲染 | 200个Draw Call | 合批渲染或GPU Instancing |
| 每个敌人独立AI | 每帧200次AI计算 | 距离剔除(远处不更新AI) |
对象池实现
对象池是解决"频繁创建和销毁"问题的经典方案。
C
// ObjectPool.cs
// 通用对象池,用于复用敌人、弹幕等频繁创建销毁的对象
using Godot;
using System.Collections.Generic;
public partial class ObjectPool<T> : Node where T : Node3D
{
// 对象模板
private PackedScene _scene;
// 池中可用的对象
private Queue<T> _available = new Queue<T>();
// 所有已分配的对象
private List<T> _active = new List<T>();
// 池的初始大小
private int _initialSize;
public ObjectPool(PackedScene scene, int initialSize = 50)
{
_scene = scene;
_initialSize = initialSize;
}
/// <summary>
/// 初始化池,预创建一批对象
/// </summary>
public void Initialize(Node parent)
{
for (int i = 0; i < _initialSize; i++)
{
var obj = _scene.Instantiate<T>();
obj.ProcessMode = ProcessModeEnum.Disabled;
parent.AddChild(obj);
_available.Enqueue(obj);
}
}
/// <summary>
/// 从池中获取一个对象
/// </summary>
public T GetObject()
{
T obj;
if (_available.Count > 0)
{
// 从池中取出
obj = _available.Dequeue();
}
else
{
// 池为空,创建新对象
obj = _scene.Instantiate<T>();
// 需要手动添加到场景树
GD.Print($"对象池扩展:创建了新的 {_scene.ResourcePath}");
}
obj.ProcessMode = ProcessModeEnum.Inherit;
_active.Add(obj);
return obj;
}
/// <summary>
/// 归还对象到池中
/// </summary>
public void ReturnObject(T obj)
{
obj.ProcessMode = ProcessModeEnum.Disabled;
_active.Remove(obj);
_available.Enqueue(obj);
}
/// <summary>
/// 获取当前活跃对象数量
/// </summary>
public int ActiveCount => _active.Count;
/// <summary>
/// 获取当前可用对象数量
/// </summary>
public int AvailableCount => _available.Count;
}
// === 使用示例 ===
// 在GameManager中:
// var enemyPool = new ObjectPool<Node3D>(enemyScene, 100);
// enemyPool.Initialize(enemyContainer);
//
// 生成敌人时:
// var enemy = enemyPool.GetObject();
// enemy.GlobalPosition = spawnPos;
//
// 敌人死亡时:
// enemyPool.ReturnObject(enemy);GDScript
# object_pool.gd
# 通用对象池,用于复用敌人、弹幕等频繁创建销毁的对象
class_name ObjectPool
extends Node
# 对象模板
var _scene: PackedScene
# 池中可用的对象
var _available = []
# 所有已分配的对象
var _active = []
# 池的初始大小
var _initial_size: int
func _init(scene: PackedScene, initial_size: int = 50):
_scene = scene
_initial_size = initial_size
## 初始化池,预创建一批对象
func initialize(parent: Node):
for i in range(_initial_size):
var obj = _scene.instantiate()
obj.process_mode = Node.PROCESS_MODE_DISABLED
parent.add_child(obj)
_available.append(obj)
## 从池中获取一个对象
func get_object() -> Node3D:
var obj: Node3D
if _available.size() > 0:
# 从池中取出
obj = _available.pop_front()
else:
# 池为空,创建新对象
obj = _scene.instantiate()
print("对象池扩展:创建了新的对象")
obj.process_mode = Node.PROCESS_MODE_INHERIT
_active.append(obj)
return obj
## 归还对象到池中
func return_object(obj: Node3D):
obj.process_mode = Node.PROCESS_MODE_DISABLED
_active.erase(obj)
_available.append(obj)
## 获取当前活跃对象数量
func get_active_count() -> int:
return _active.size()
## 获取当前可用对象数量
func get_available_count() -> int:
return _available.size()
# === 使用示例 ===
# var enemy_pool = ObjectPool.new(enemy_scene, 100)
# enemy_pool.initialize(enemy_container)
#
# 生成敌人时:
# var enemy = enemy_pool.get_object()
# enemy.global_position = spawn_pos
#
# 敌人死亡时:
# enemy_pool.return_object(enemy)距离剔除
远离玩家的敌人不需要每帧都更新AI和动画。
| 距离 | 更新频率 | AI | 动画 | 物理 |
|---|---|---|---|---|
| 0~30(近) | 每帧 | 完整AI | 播放 | 完整 |
| 30~60(中) | 每3帧 | 简化AI | 冻结 | 简化 |
| 60+(远) | 每10帧 | 不更新 | 冻结 | 不更新 |
距离剔除的原理
就像一个老师管理一个班级:坐在前排的学生(近处敌人),老师能看清每个人的动作;坐在后排的学生(远处敌人),老师只偶尔看一眼。这样老师的精力(CPU)就不会被分散。
UI 美化
HUD 界面布局
┌───────────────────────────────────────┐
│ ⏱ 05:23 难度:普通 │ ← 顶部信息栏
│ │
│ ❤ [████████░░] 80/100 │ ← 血条
│ ⭐ [██████░░░░] Lv.12 │ ← 经验条
│ │
│ ┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐ │ ← 武器栏
│ │🗡││🔫││🔥││⚡││ ││ │ │
│ │Lv3││Lv5││Lv2││Lv1││ ││ │ │
│ └─┘└─┘└─┘└─┘└─┘└─┘ │
│ │
│ 🧙 (玩家) │
│ 💀💀 💀 │
│ 💀 🗡️ 💀💀 │
│ │
│ ┌──────────────────┐ │ ← 小地图
│ │ · 🧙 │ │
│ │ · · · │ │
│ └──────────────────┘ │
└───────────────────────────────────────┘UI 颜色方案
| UI元素 | 颜色 | 说明 |
|---|---|---|
| 血条 | 红色 → 黄色(低血量) | 低于30%时变黄闪烁 |
| 经验条 | 蓝色 | 从左到右填充 |
| 武器栏背景 | 半透明黑色 | 不遮挡游戏画面 |
| 武器图标边框 | 白色/蓝色/金色 | 对应普通/稀有/传说 |
| 计时器 | 白色大字 | 始终清晰可见 |
| Boss警告 | 红色闪烁 | 全屏红色半透明覆盖 |
血条和经验条脚本
C
// HUD.cs
// 游戏界面HUD
using Godot;
public partial class HUD : CanvasLayer
{
// === UI节点引用 ===
private Label _timerLabel;
private ProgressBar _healthBar;
private Label _healthText;
private ProgressBar _expBar;
private Label _levelLabel;
private Label _killCountLabel;
private Label _difficultyLabel;
// === 引用 ===
private Node _player;
private SurvivalTimer _timer;
private ExperienceManager _expManager;
private DifficultyManager _diffManager;
public override void _Ready()
{
// 获取UI节点
_timerLabel = GetNode<Label>("TopBar/TimerLabel");
_healthBar = GetNode<ProgressBar>("StatusBars/HealthBar");
_healthText = GetNode<Label>("StatusBars/HealthText");
_expBar = GetNode<ProgressBar>("StatusBars/ExpBar");
_levelLabel = GetNode<Label>("StatusBars/LevelLabel");
_killCountLabel = GetNode<Label>("TopBar/KillLabel");
_difficultyLabel = GetNode<Label>("TopBar/DiffLabel");
// 获取游戏系统引用
var scene = GetTree().CurrentScene;
_player = scene.GetNodeOrNull("Player");
_timer = scene.GetNodeOrNull<SurvivalTimer>("SurvivalTimer");
_expManager = scene.GetNodeOrNull<ExperienceManager>(
"ExperienceManager");
_diffManager = scene.GetNodeOrNull<DifficultyManager>(
"DifficultyManager");
// 连接信号
if (_timer != null)
{
_timer.TimeUpdated += OnTimeUpdated;
}
if (_expManager != null)
{
_expManager.ExperienceChanged += OnExpChanged;
_expManager.LevelUp += OnLevelUp;
}
}
public override void _Process(double delta)
{
UpdateHealthBar();
UpdateKillCount();
UpdateDifficultyLabel();
}
private void UpdateHealthBar()
{
if (_player == null || !_player.HasMethod("GetHealthPercent"))
return;
float percent = (float)_player.Call("GetHealthPercent");
_healthBar.Value = percent * 100;
// 低血量时改变颜色
if (percent < 0.3f)
{
_healthBar.Modulate = Colors.Yellow;
// 闪烁效果
float flash = (float)(GD.Randf() > 0.5 ? 1.0 : 0.7);
_healthBar.Modulate = new Color(1, flash, 0);
}
else
{
_healthBar.Modulate = Colors.White;
}
}
private void OnTimeUpdated(string formattedTime)
{
_timerLabel.Text = formattedTime;
}
private void OnExpChanged(float current, float required)
{
_expBar.MaxValue = required;
_expBar.Value = current;
}
private void OnLevelUp(int newLevel)
{
_levelLabel.Text = $"Lv.{newLevel}";
// 升级特效(放大后缩小)
var tween = CreateTween();
tween.TweenProperty(_levelLabel, "scale",
new Vector2(1.5f, 1.5f), 0.1f);
tween.TweenProperty(_levelLabel, "scale",
Vector2.One, 0.3f);
}
private void UpdateKillCount()
{
if (_timer == null) return;
_killCountLabel.Text = $"击杀: {_timer.KillCount}";
}
private void UpdateDifficultyLabel()
{
if (_diffManager == null) return;
_difficultyLabel.Text =
_diffManager.GetDifficultyDescription();
}
}GDScript
# hud.gd
# 游戏界面HUD
extends CanvasLayer
# === UI节点引用 ===
var _timer_label: Label
var _health_bar: ProgressBar
var _health_text: Label
var _exp_bar: ProgressBar
var _level_label: Label
var _kill_count_label: Label
var _difficulty_label: Label
# === 引用 ===
var _player: Node
var _timer: Node
var _exp_manager: Node
var _diff_manager: Node
func _ready():
# 获取UI节点
_timer_label = get_node("TopBar/TimerLabel")
_health_bar = get_node("StatusBars/HealthBar")
_health_text = get_node("StatusBars/HealthText")
_exp_bar = get_node("StatusBars/ExpBar")
_level_label = get_node("StatusBars/LevelLabel")
_kill_count_label = get_node("TopBar/KillLabel")
_difficulty_label = get_node("TopBar/DiffLabel")
# 获取游戏系统引用
var scene = get_tree().current_scene
_player = scene.get_node_or_null("Player")
_timer = scene.get_node_or_null("SurvivalTimer")
_exp_manager = scene.get_node_or_null("ExperienceManager")
_diff_manager = scene.get_node_or_null("DifficultyManager")
# 连接信号
if _timer:
_timer.time_updated.connect(_on_time_updated)
if _exp_manager:
_exp_manager.experience_changed.connect(_on_exp_changed)
_exp_manager.level_up.connect(_on_level_up)
func _process(delta):
update_health_bar()
update_kill_count()
update_difficulty_label()
func update_health_bar():
if _player == null or not _player.has_method("get_health_percent"):
return
var percent = _player.get_health_percent()
_health_bar.value = percent * 100
# 低血量时改变颜色
if percent < 0.3:
var flash = 1.0 if randf() > 0.5 else 0.7
_health_bar.modulate = Color(1, flash, 0)
else:
_health_bar.modulate = Color.WHITE
func _on_time_updated(formatted_time: String):
_timer_label.text = formatted_time
func _on_exp_changed(current: float, required: float):
_exp_bar.max_value = required
_exp_bar.value = current
func _on_level_up(new_level: int):
_level_label.text = "Lv.%d" % new_level
# 升级特效
var tween = create_tween()
tween.tween_property(_level_label, "scale",
Vector2(1.5, 1.5), 0.1)
tween.tween_property(_level_label, "scale",
Vector2.ONE, 0.3)
func update_kill_count():
if _timer == null:
return
_kill_count_label.text = "击杀: %d" % _timer.kill_count
func update_difficulty_label():
if _diff_manager == null:
return
_difficulty_label.text = _diff_manager.get_difficulty_description()音效系统
音效清单
| 类别 | 音效名称 | 触发时机 | 重要性 |
|---|---|---|---|
| 武器 | 旋转刀片声 | 刀片旋转时循环播放 | 中 |
| 武器 | 弹幕发射声 | 每次发射弹幕 | 高 |
| 武器 | 火焰声 | 火焰AOE持续播放 | 中 |
| 敌人 | 敌人死亡声 | 敌人被消灭 | 高 |
| 敌人 | Boss出场声 | Boss出现时 | 高 |
| 玩家 | 受伤声 | 玩家被攻击 | 高 |
| 玩家 | 升级声 | 升级时 | 高 |
| UI | 拾取声 | 捡起经验宝石 | 中 |
| UI | 按钮点击声 | 点击UI按钮 | 低 |
| BGM | 背景音乐 | 游戏全程循环 | 高 |
音效的"打击感"
打击感很大一部分来自音效。一个好的"啪"声可以让简单的方块碰撞变得很有力。建议在武器命中敌人时播放一个短促的打击音效,同时配合轻微的屏幕震动。
简单音效管理器
C
// AudioManager.cs
// 音效管理器
using Godot;
using System.Collections.Generic;
public partial class AudioManager : Node
{
// 单例模式
public static AudioManager Instance { get; private set; }
// 音效资源
[Export] public AudioStream HitSound;
[Export] public AudioStream EnemyDeathSound;
[Export] public AudioStream LevelUpSound;
[Export] public AudioStream PickupSound;
[Export] public AudioStream PlayerHurtSound;
[Export] public AudioStream BossAppearSound;
[Export] public AudioStream ButtonClickSound;
// 背景音乐
[Export] public AudioStream BGM;
private AudioStreamPlayer _bgmPlayer;
private List<AudioStreamPlayer> _sfxPlayers = new();
// 同时播放的最大音效数
[Export] public int MaxSFXPlayers = 8;
public override void _Ready()
{
if (Instance == null)
Instance = this;
// 创建BGM播放器
_bgmPlayer = new AudioStreamPlayer();
_bgmPlayer.Stream = BGM;
_bgmPlayer.VolumeDb = -10f; // BGM音量较低
AddChild(_bgmPlayer);
// 预创建SFX播放器池
for (int i = 0; i < MaxSFXPlayers; i++)
{
var player = new AudioStreamPlayer();
player.VolumeDb = -5f;
AddChild(player);
_sfxPlayers.Add(player);
// 播放完毕后自动标记为可用
player.Finished += () => { /* 标记可用 */ };
}
}
/// <summary>
/// 播放背景音乐
/// </summary>
public void PlayBGM()
{
if (_bgmPlayer != null && !_bgmPlayer.Playing)
{
_bgmPlayer.Play();
}
}
/// <summary>
/// 播放音效
/// </summary>
public void PlaySFX(AudioStream sound)
{
if (sound == null) return;
// 找到一个空闲的播放器
foreach (var player in _sfxPlayers)
{
if (!player.Playing)
{
player.Stream = sound;
player.Play();
return;
}
}
// 所有播放器都在用,强制使用第一个
_sfxPlayers[0].Stream = sound;
_sfxPlayers[0].Play();
}
// 便捷方法
public void PlayHit() => PlaySFX(HitSound);
public void PlayEnemyDeath() => PlaySFX(EnemyDeathSound);
public void PlayLevelUp() => PlaySFX(LevelUpSound);
public void PlayPickup() => PlaySFX(PickupSound);
public void PlayPlayerHurt() => PlaySFX(PlayerHurtSound);
public void PlayBossAppear() => PlaySFX(BossAppearSound);
public void PlayButtonClick() => PlaySFX(ButtonClickSound);
}GDScript
# audio_manager.gd
# 音效管理器
extends Node
# 单例模式
static var instance: AudioManager
# 音效资源
@export var hit_sound: AudioStream
@export var enemy_death_sound: AudioStream
@export var level_up_sound: AudioStream
@export var pickup_sound: AudioStream
@export var player_hurt_sound: AudioStream
@export var boss_appear_sound: AudioStream
@export var button_click_sound: AudioStream
# 背景音乐
@export var bgm: AudioStream
var _bgm_player: AudioStreamPlayer
var _sfx_players = []
# 同时播放的最大音效数
@export var max_sfx_players: int = 8
func _ready():
if instance == null:
instance = self
# 创建BGM播放器
_bgm_player = AudioStreamPlayer.new()
_bgm_player.stream = bgm
_bgm_player.volume_db = -10.0 # BGM音量较低
add_child(_bgm_player)
# 预创建SFX播放器池
for i in range(max_sfx_players):
var player = AudioStreamPlayer.new()
player.volume_db = -5.0
add_child(player)
_sfx_players.append(player)
## 播放背景音乐
func play_bgm():
if _bgm_player and not _bgm_player.playing:
_bgm_player.play()
## 播放音效
func play_sfx(sound: AudioStream):
if sound == null:
return
# 找到一个空闲的播放器
for player in _sfx_players:
if not player.playing:
player.stream = sound
player.play()
return
# 所有播放器都在用,强制使用第一个
_sfx_players[0].stream = sound
_sfx_players[0].play()
# 便捷方法
func play_hit(): play_sfx(hit_sound)
func play_enemy_death(): play_sfx(enemy_death_sound)
func play_level_up(): play_sfx(level_up_sound)
func play_pickup(): play_sfx(pickup_sound)
func play_player_hurt(): play_sfx(player_hurt_sound)
func play_boss_appear(): play_sfx(boss_appear_sound)
func play_button_click(): play_sfx(button_click_sound)移动端适配
虚拟摇杆
手机上没有键盘,需要一个虚拟摇杆来控制移动。
| 适配项 | 说明 |
|---|---|
| 虚拟摇杆 | 左下角显示一个圆形摇杆 |
| 触控区域 | 摇杆响应区域要足够大 |
| UI缩放 | 根据屏幕分辨率自动缩放 |
| 帧率 | 移动端建议锁定30FPS以省电 |
移动端性能
手机性能有限,建议:
- 最大敌人数降低到100(PC是200)
- 关闭阴影
- 降低粒子效果数量
- 使用Mobile渲染器
多平台导出
导出配置
| 平台 | 分辨率 | 帧率目标 | 特殊设置 |
|---|---|---|---|
| Windows | 1280x720 | 60FPS | 无 |
| macOS | 1280x720 | 60FPS | 无 |
| Linux | 1280x720 | 60FPS | 无 |
| Android | 自适应 | 30FPS | 触控模式 |
| iOS | 自适应 | 30FPS | 触控模式 |
| Web | 自适应 | 60FPS | 文件体积限制 |
导出步骤
- 打开 项目 -> 导出
- 添加目标平台
- 配置平台选项
- 点击"导出项目"
Web导出注意事项
导出到网页时,所有资源会被打包成一个文件。如果游戏超过 100MB,需要考虑压缩资源(特别是音效和3D模型)。
设置菜单
可调选项
| 设置项 | 选项 | 默认值 |
|---|---|---|
| 音量 - 主音量 | 0~100% | 80% |
| 音量 - BGM | 0~100% | 70% |
| 音量 - 音效 | 0~100% | 100% |
| 画面质量 | 低/中/高 | 高 |
| 屏幕震动 | 开/关 | 开 |
| 全屏 | 开/关 | 关 |
| 分辨率 | 720p/1080p/1440p | 720p |
C
// SettingsManager.cs
// 游戏设置管理器
using Godot;
public partial class SettingsManager : Node
{
// 设置数据
public float MasterVolume { get; set; } = 0.8f;
public float BGMVolume { get; set; } = 0.7f;
public float SFXVolume { get; set; } = 1.0f;
public bool ScreenShake { get; set; } = true;
public bool Fullscreen { get; set; } = false;
public Vector2I Resolution { get; set; } = new Vector2I(1280, 720);
// 设置文件路径
private const string SettingsPath = "user://settings.json";
public override void _Ready()
{
LoadSettings();
ApplySettings();
}
/// <summary>
/// 保存设置到文件
/// </summary>
public void SaveSettings()
{
var settings = new Godot.Collections.Dictionary
{
["master_volume"] = MasterVolume,
["bgm_volume"] = BGMVolume,
["sfx_volume"] = SFXVolume,
["screen_shake"] = ScreenShake,
["fullscreen"] = Fullscreen,
["resolution_x"] = Resolution.X,
["resolution_y"] = Resolution.Y
};
string json = Json.Stringify(settings, "\t");
var file = FileAccess.Open(SettingsPath,
FileAccess.ModeFlags.Write);
file?.StoreString(json);
file?.Close();
}
/// <summary>
/// 从文件加载设置
/// </summary>
public void LoadSettings()
{
if (!FileAccess.FileExists(SettingsPath)) return;
var file = FileAccess.Open(SettingsPath,
FileAccess.ModeFlags.Read);
string json = file?.GetAsText();
file?.Close();
if (string.IsNullOrEmpty(json)) return;
var data = Json.ParseString(json)
as Godot.Collections.Dictionary;
if (data == null) return;
MasterVolume = (float)(data["master_volume"] ?? 0.8f);
BGMVolume = (float)(data["bgm_volume"] ?? 0.7f);
SFXVolume = (float)(data["sfx_volume"] ?? 1.0f);
ScreenShake = (bool)(data["screen_shake"] ?? true);
Fullscreen = (bool)(data["fullscreen"] ?? false);
int rx = (int)(data["resolution_x"] ?? 1280);
int ry = (int)(data["resolution_y"] ?? 720);
Resolution = new Vector2I(rx, ry);
}
/// <summary>
/// 应用设置到游戏
/// </summary>
public void ApplySettings()
{
// 设置音量
var busIdx = AudioServer.GetBusIndex("Master");
AudioServer.SetBusVolumeDb(busIdx,
Mathf.LinearToDb(MasterVolume));
var bgmIdx = AudioServer.GetBusIndex("BGM");
AudioServer.SetBusVolumeDb(bgmIdx,
Mathf.LinearToDb(BGMVolume));
var sfxIdx = AudioServer.GetBusIndex("SFX");
AudioServer.SetBusVolumeDb(sfxIdx,
Mathf.LinearToDb(SFXVolume));
// 设置全屏
DisplayServer.WindowSetMode(
Fullscreen
? DisplayServer.WindowMode.Fullscreen
: DisplayServer.WindowMode.Maximized);
// 设置分辨率
DisplayServer.WindowSetSize(Resolution);
}
}GDScript
# settings_manager.gd
# 游戏设置管理器
extends Node
# 设置数据
var master_volume: float = 0.8
var bgm_volume: float = 0.7
var sfx_volume: float = 1.0
var screen_shake: bool = true
var fullscreen: bool = false
var resolution: Vector2i = Vector2i(1280, 720)
# 设置文件路径
const SETTINGS_PATH = "user://settings.json"
func _ready():
load_settings()
apply_settings()
## 保存设置到文件
func save_settings():
var settings = {
"master_volume": master_volume,
"bgm_volume": bgm_volume,
"sfx_volume": sfx_volume,
"screen_shake": screen_shake,
"fullscreen": fullscreen,
"resolution_x": resolution.x,
"resolution_y": resolution.y
}
var json = JSON.stringify(settings, "\t")
var file = FileAccess.open(SETTINGS_PATH, FileAccess.WRITE)
if file:
file.store_string(json)
file.close()
## 从文件加载设置
func load_settings():
if not FileAccess.file_exists(SETTINGS_PATH):
return
var file = FileAccess.open(SETTINGS_PATH, FileAccess.READ)
var json = file.get_as_text() if file else ""
if file:
file.close()
if json.is_empty():
return
var data = JSON.parse_string(json)
if data == null:
return
master_volume = data.get("master_volume", 0.8)
bgm_volume = data.get("bgm_volume", 0.7)
sfx_volume = data.get("sfx_volume", 1.0)
screen_shake = data.get("screen_shake", true)
fullscreen = data.get("fullscreen", false)
var rx = data.get("resolution_x", 1280)
var ry = data.get("resolution_y", 720)
resolution = Vector2i(rx, ry)
## 应用设置到游戏
func apply_settings():
# 设置音量
var bus_idx = AudioServer.get_bus_index("Master")
AudioServer.set_bus_volume_db(bus_idx,
linear_to_db(master_volume))
var bgm_idx = AudioServer.get_bus_index("BGM")
AudioServer.set_bus_volume_db(bgm_idx,
linear_to_db(bgm_volume))
var sfx_idx = AudioServer.get_bus_index("SFX")
AudioServer.set_bus_volume_db(sfx_idx,
linear_to_db(sfx_volume))
# 设置全屏
DisplayServer.window_set_mode(
DisplayServer.WINDOW_MODE_FULLSCREEN if fullscreen \
else DisplayServer.WINDOW_MODE_MAXIMIZED)
# 设置分辨率
DisplayServer.window_set_size(resolution)发布前检查清单
在正式发布之前,请逐项检查以下内容:
| 检查项 | 状态 | 说明 |
|---|---|---|
| 游戏能从头玩到尾 | 必须通过 | 开始 → 升级 → Boss → 死亡 → 重新开始 |
| 没有崩溃和报错 | 必须通过 | 运行30分钟无崩溃 |
| 帧率稳定在30+ | 必须通过 | 大量敌人时不低于30FPS |
| 音效正常播放 | 建议通过 | 所有音效和BGM正常 |
| 设置能保存和加载 | 建议通过 | 关闭重开后设置保留 |
| UI在不同分辨率下正常 | 建议通过 | 至少测试720p和1080p |
| 没有内存泄漏 | 建议通过 | 长时间运行内存不持续增长 |
总结
本章我们完成了游戏的最后打磨:
- 性能优化 — 对象池、距离剔除
- UI美化 — HUD布局、血条经验条、颜色方案
- 音效系统 — 武器音效、背景音乐、音效管理器
- 移动端适配 — 虚拟摇杆、触控优化
- 多平台导出 — Windows/Mac/Linux/Android/iOS/Web
- 设置菜单 — 音量、画面、分辨率等选项
- 发布检查 — 发布前的完整检查清单
恭喜你!到这里,一个完整的 2.5D 割草游戏就开发完成了。从项目搭建到角色移动、自动战斗、敌人生成、升级系统、道具装备、地图生成、生存计时,一直到最后的打磨发布——你已经掌握了割草游戏开发的所有核心知识。
接下来就是不断迭代和优化,让你的游戏变得更加好玩。祝你好运!
回到开头 → 1. 割草——核心玩法设计
