10. 打磨与发布
2026/4/14大约 7 分钟
10. 咸鱼之王——打磨与发布
10.1 数值平衡
数值平衡是放置游戏最关键的"打磨"工作。如果金币产出太快,玩家很快就没有目标感;如果太慢,玩家会觉得无聊。
平衡的核心公式
放置游戏的数值设计围绕一个核心问题:玩家多久能升一级?多久能推一关?
| 指标 | 推荐值 | 说明 |
|---|---|---|
| 升级英雄时间 | 30秒~2分钟 | 太快没成就感,太慢无聊 |
| 推一关时间 | 5~30秒 | 前期快后期慢 |
| 抽一次卡时间 | 30秒~1分钟 | 攒够100金币的时间 |
| 离线8小时收益 | 约等于在线1小时 | 给玩家回来看的动力 |
数值平衡表
C
/// <summary>
/// 数值平衡表——定义各阶段的数值参数
/// </summary>
public static class BalanceTable
{
/// <summary>
/// 根据关卡阶段获取难度系数
/// </summary>
public static float GetDifficultyMultiplier(int stageId)
{
if (stageId <= 30) return 1.0f + (stageId - 1) * 0.15f; // 前期缓慢
if (stageId <= 60) return 1.0f + (stageId - 1) * 0.2f; // 中期中等
if (stageId <= 100) return 1.0f + (stageId - 1) * 0.3f; // 后期快速
return 1.0f + (stageId - 1) * 0.5f; // 后期指数
}
/// <summary>
/// 检查当前数值是否平衡
/// </summary>
public static string CheckBalance(int highestStage, float partyDps)
{
var issues = new System.Collections.Generic.List<string>();
// 检查1:推图速度是否合理
var nextStage = StageData.Generate(highestStage + 1);
float timeToKill = nextStage.EnemyHp / Mathf.Max(partyDps, 0.1f);
if (timeToKill > 60f)
issues.Add($"下一关预计击杀时间{timeToKill:F0}秒,可能太慢");
if (timeToKill < 3f)
issues.Add($"下一关预计击杀时间{timeToKill:F0}秒,可能太快");
// 检查2:金币产出是否合理
float income = IncomeCalculator.CalculateIncomePerSecond(
highestStage, 0f, 0f, 0f);
int upgradeCost = 10 + 1 * 1; // 1级升2级的费用
if (income > 0)
{
float timeToAffordUpgrade = upgradeCost / income;
if (timeToAffordUpgrade > 30f)
issues.Add($"攒够一次升级需要{timeToAffordUpgrade:F0}秒,可能太慢");
}
// 检查3:离线收益是否合理
int offlineGold = IncomeCalculator.CalculateOfflineIncome(
highestStage, 0f, 8 * 3600);
if (offlineGold < 100)
issues.Add("离线8小时收益不足100金币,动力不够");
return issues.Count > 0
? string.Join("\n", issues)
: "数值平衡正常";
}
}GDScript
## 数值平衡表——定义各阶段的数值参数
class_name BalanceTable
## 根据关卡阶段获取难度系数
static func get_difficulty_multiplier(stage_id: int) -> float:
if stage_id <= 30:
return 1.0 + (stage_id - 1) * 0.15
elif stage_id <= 60:
return 1.0 + (stage_id - 1) * 0.2
elif stage_id <= 100:
return 1.0 + (stage_id - 1) * 0.3
else:
return 1.0 + (stage_id - 1) * 0.5
## 检查数值是否平衡
static func check_balance(highest_stage: int, party_dps: float) -> String:
var issues: Array[String] = []
# 推图速度
var next_stage = StageData.generate(highest_stage + 1)
var time_to_kill: float = float(next_stage.enemy_hp) / maxf(party_dps, 0.1)
if time_to_kill > 60.0:
issues.append("下一关击杀时间%.0f秒,太慢" % time_to_kill)
if time_to_kill < 3.0:
issues.append("下一关击杀时间%.0f秒,太快" % time_to_kill)
# 金币产出
var income: float = IncomeCalculator.calculate_income_per_second(
highest_stage, 0.0, 0.0, 0.0)
var upgrade_cost: int = 10 + 1
if income > 0:
var time_to_afford: float = float(upgrade_cost) / income
if time_to_afford > 30.0:
issues.append("攒够一次升级需要%.0f秒,太慢" % time_to_afford)
# 离线收益
var offline_gold: int = IncomeCalculator.calculate_offline_income(
highest_stage, 0.0, 8.0 * 3600.0)
if offline_gold < 100:
issues.append("离线8小时收益不足100金币")
if issues.size() > 0:
return "\n".join(issues)
return "数值平衡正常"10.2 留存设计
留存(Retention)是指玩家持续回到游戏的比例。放置游戏通过以下机制来提升留存:
| 机制 | 说明 | 目的 |
|---|---|---|
| 每日签到 | 每天登录领奖励 | 培养登录习惯 |
| 七日目标 | 前7天每天有特殊任务 | 新手引导 |
| 每日任务 | 每天重置的小任务 | 增加日活 |
| 离线收益 | 离线回来有惊喜 | 给回来看的理由 |
每日签到系统
C
using Godot;
using System;
/// <summary>
/// 每日签到管理器
/// </summary>
public partial class DailyCheckinManager : Node
{
private string _lastCheckinDate = "";
private int _consecutiveDays = 0;
// 签到奖励:第1天50金,第2天100金...第7天500金+1次免费抽卡
private static readonly int[] DayRewards = { 50, 100, 150, 200, 300, 400, 500 };
/// <summary>
/// 检查今天是否已签到
/// </summary>
public bool HasCheckedInToday()
{
string today = DateTime.Now.ToString("yyyy-MM-dd");
return _lastCheckinDate == today;
}
/// <summary>
/// 执行签到
/// </summary>
public (int goldReward, bool freeDraw) CheckIn()
{
if (HasCheckedInToday())
return (0, false);
string today = DateTime.Now.ToString("yyyy-MM-dd");
string yesterday = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd");
// 检查是否连续签到
if (_lastCheckinDate == yesterday)
_consecutiveDays++;
else
_consecutiveDays = 1;
_lastCheckinDate = today;
// 计算奖励(7天一个循环)
int dayIndex = ((_consecutiveDays - 1) % 7);
int gold = DayRewards[dayIndex];
bool freeDraw = (dayIndex == 6); // 第7天送免费抽卡
GameManager.Instance.AddGold(gold);
GD.Print($"签到成功!连续{_consecutiveDays}天,获得 {gold} 金币" +
(freeDraw ? " + 1次免费抽卡" : ""));
return (gold, freeDraw);
}
/// <summary>
/// 获取连续签到天数
/// </summary>
public int GetConsecutiveDays() => _consecutiveDays;
}GDScript
extends Node
## 每日签到管理器
var _last_checkin_date: String = ""
var _consecutive_days: int = 0
## 签到奖励
const DAY_REWARDS: Array[int] = [50, 100, 150, 200, 300, 400, 500]
## 检查今天是否已签到
func has_checked_in_today() -> bool:
var today: String = Time.get_date_string_from_system()
return _last_checkin_date == today
## 执行签到
func check_in() -> Dictionary:
if has_checked_in_today():
return {"gold": 0, "free_draw": false}
var today: String = Time.get_date_string_from_system()
# 检查连续签到
if _last_checkin_date != "":
_consecutive_days += 1
else:
_consecutive_days = 1
_last_checkin_date = today
# 计算奖励(7天循环)
var day_index: int = (_consecutive_days - 1) % 7
var gold: int = DAY_REWARDS[day_index]
var free_draw: bool = (day_index == 6)
GameManager._gold += gold
print("签到成功!连续%d天,获得 %d 金币" % [_consecutive_days, gold])
if free_draw:
print(" + 1次免费抽卡")
return {"gold": gold, "free_draw": free_draw}
## 获取连续签到天数
func get_consecutive_days() -> int:
return _consecutive_days10.3 商业化设计
放置游戏的商业化(赚钱)方式通常比较温和:
| 方式 | 说明 | 价格 |
|---|---|---|
| 去广告 | 移除横幅广告 | 6元一次性 |
| 月卡 | 每天额外领取100钻石 | 18元/月 |
| 新手礼包 | 金币+钻石+稀有英雄 | 12元一次性 |
| 成长基金 | 随等级解锁奖励 | 30元一次性 |
10.4 导出发布
导出目标平台
| 平台 | 用途 | 注意事项 |
|---|---|---|
| Android | 主要发布平台 | 适配不同屏幕尺寸 |
| iOS | 次要发布平台 | 需要Apple开发者账号 |
| HTML5 | 网页试玩版 | 性能有限,简化特效 |
| Windows | 桌面版 | 竖屏显示,两侧留黑边 |
发布前检查清单
| 检查项 | 说明 |
|---|---|
| 数值平衡 | 前30关不会太卡,后期有挑战 |
| 存档正常 | 自动存档、离线收益计算正确 |
| 无崩溃 | 反复操作不会闪退 |
| 性能流畅 | 低端手机也能跑60帧 |
| 广告正常 | 广告展示和关闭按钮正常 |
| 网络断开 | 离线模式下核心功能可用 |
10.5 自动存档完整实现
C
using Godot;
/// <summary>
/// 放置游戏存档管理器
/// </summary>
public partial class IdleSaveManager : Node
{
private const string SavePath = "user://idle_save.json";
/// <summary>
/// 保存游戏
/// </summary>
public void SaveGame()
{
var saveData = new Godot.Collections.Dictionary
{
{ "timestamp", Time.GetDatetimeStringFromSystem() },
{ "gold", GameManager.Instance.Gold },
{ "diamonds", GameManager.Instance.Diamonds },
{ "highest_stage", GameManager.Instance.HighestStage },
{ "current_stage", GameManager.Instance.CurrentStage },
{ "heroes", SerializeHeroes() },
{ "checkin_date", GetCheckinDate() }
};
string json = Json.Stringify(saveData, " ");
var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Write);
file.StoreString(json);
file.Close();
}
/// <summary>
/// 加载游戏
/// </summary>
public bool LoadGame()
{
if (!FileAccess.FileExists(SavePath)) return false;
var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Read);
string json = file.GetAsText();
file.Close();
var parser = new Json();
if (parser.Parse(json) != Error.Ok) return false;
var data = (Godot.Collections.Dictionary)parser.Data;
// 恢复数据
GameManager.Instance._gold = (int)data["gold"];
GameManager.Instance._diamonds = (int)data["diamonds"];
GameManager.Instance._highestStage = (int)data["highest_stage"];
GameManager.Instance._currentStage = (int)data["current_stage"];
GD.Print("存档加载成功");
return true;
}
/// <summary>
/// 检查是否有存档
/// </summary>
public bool HasSave() => FileAccess.FileExists(SavePath);
private Godot.Collections.Array SerializeHeroes()
{
var arr = new Godot.Collections.Array();
// 序列化英雄数据...
return arr;
}
private string GetCheckinDate()
{
return ""; // 从签到管理器获取
}
}GDScript
extends Node
## 放置游戏存档管理器
const SAVE_PATH := "user://idle_save.json"
## 保存游戏
func save_game() -> void:
var save_data := {
"timestamp": Time.get_datetime_string_from_system(),
"gold": GameManager.gold,
"diamonds": GameManager.diamonds,
"highest_stage": GameManager.highest_stage,
"current_stage": GameManager.current_stage,
}
var json := JSON.stringify(save_data, " ")
var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
file.store_string(json)
file.close()
## 加载游戏
func load_game() -> bool:
if not FileAccess.file_exists(SAVE_PATH):
return false
var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
var json := file.get_as_text()
file.close()
var parser = JSON.new()
if parser.parse(json) != OK:
return false
var data = parser.data
GameManager._gold = data.get("gold", 0)
GameManager._diamonds = data.get("diamonds", 0)
GameManager._highest_stage = data.get("highest_stage", 1)
GameManager._current_stage = data.get("current_stage", 1)
print("存档加载成功")
return true
## 检查是否有存档
func has_save() -> bool:
return FileAccess.file_exists(SAVE_PATH)10.6 本章小结
| 知识点 | 说明 |
|---|---|
| 数值平衡 | 升级时间、推图速度、金币产出的平衡检查 |
| 留存设计 | 每日签到、七日目标、离线收益 |
| 商业化 | 去广告、月卡、礼包等温和变现方式 |
| 自动存档 | JSON序列化,30秒自动保存 |
| 导出发布 | Android/iOS/HTML5/Windows多平台 |
到这里,咸鱼之王的所有核心系统都已完成。从核心循环设计到数值平衡,从英雄收集到自动战斗,你学会了如何用Godot制作一款完整的放置挂机RPG。
放置游戏看似简单,但背后的数值设计是一门深奥的学问。希望你能在这个基础上,做出自己独一无二的放置游戏!
