2. 项目搭建
2026/4/14大约 6 分钟
2. 咸鱼之王——项目搭建
2.1 项目规划
放置挂机游戏的项目结构比RPG简单很多——没有复杂的地图系统、对话系统、任务系统,核心就是英雄管理、自动战斗、收益计算三个模块。
创建Godot项目
- 打开Godot 4.x,新建2D项目
- 项目名称:
IdleFishKing - 窗口大小:720 x 1280(手机竖屏比例)
项目设置
| 设置项 | 值 | 说明 |
|---|---|---|
| 应用程序 → 名称 | 咸鱼之王 | 游戏显示名称 |
| 显示 → 窗口 → 宽度 | 720 | 手机竖屏宽度 |
| 显示 → 窗口 → 高度 | 1280 | 手机竖屏高度 |
| 输入映射 | tap | 触屏点击 |
2.2 目录结构
res://
├── scenes/
│ ├── main.tscn ← 主场景
│ ├── battle/
│ │ └── auto_battle.tscn ← 自动战斗场景
│ ├── heroes/
│ │ └── hero_card.tscn ← 英雄卡片UI
│ ├── stages/
│ │ └── stage_select.tscn ← 关卡选择
│ └── ui/
│ ├── main_menu.tscn ← 主菜单
│ ├── hero_list_ui.tscn ← 英雄列表
│ ├── battle_ui.tscn ← 战斗UI
│ ├── income_popup.tscn ← 收益弹窗
│ └── settings_ui.tscn ← 设置
├── scripts/
│ ├── managers/
│ │ ├── game_manager.gd/.cs ← 全局管理器
│ │ ├── hero_manager.gd/.cs ← 英雄管理器
│ │ ├── battle_manager.gd/.cs ← 战斗管理器
│ │ ├── income_manager.gd/.cs ← 收益管理器
│ │ └── save_manager.gd/.cs ← 存档管理器
│ ├── battle/
│ │ ├── auto_battle.gd/.cs ← 自动战斗逻辑
│ │ └── battle_unit.gd/.cs ← 战斗单位
│ ├── heroes/
│ │ └── hero_database.gd/.cs ← 英雄数据库
│ ├── stages/
│ │ └── stage_database.gd/.cs ← 关卡数据库
│ └── data/
│ └── constants.gd/.cs ← 常量定义
├── assets/
│ ├── sprites/
│ │ ├── heroes/ ← 英雄图片
│ │ ├── enemies/ ← 怪物图片
│ │ └── ui/ ← UI素材
│ ├── fonts/
│ └── audio/
│ ├── music/
│ └── sfx/
└── autoload/
└── game_manager.gd/.cs ← 自动加载2.3 常量定义
C
/// <summary>
/// 咸鱼之王游戏常量
/// </summary>
public static class IdleConstants
{
// ===== 收益 =====
/// <summary>基础每秒金币</summary>
public const int BaseIncomePerSecond = 5;
/// <summary>每推一关增加的每秒金币</summary>
public const int IncomePerStage = 2;
/// <summary>离线收益效率(50%)</summary>
public const float OfflineEfficiency = 0.5f;
/// <summary>最大离线时间(秒)——8小时</summary>
public const int MaxOfflineSeconds = 8 * 3600;
/// <summary>自动存档间隔(秒)</summary>
public const float AutoSaveInterval = 30f;
// ===== 在线时长奖励 =====
/// <summary>在线时长奖励间隔(秒)——每10分钟</summary>
public const int OnlineBonusInterval = 600;
/// <summary>每次在线时长奖励增加的收益比例(5%)</summary>
public const float OnlineBonusPerInterval = 0.05f;
/// <summary>在线时长奖励上限(50%)</summary>
public const float MaxOnlineBonus = 0.5f;
// ===== 英雄 =====
/// <summary>最大英雄数量</summary>
public const int MaxHeroes = 6;
/// <summary>初始英雄等级</summary>
public const int HeroStartingLevel = 1;
// ===== 关卡 =====
/// <summary>每章关数</summary>
public const int StagesPerChapter = 10;
// ===== 装备 =====
/// <summary>装备最大强化等级</summary>
public const int MaxEnhanceLevel = 15;
// ===== 抽卡 =====
/// <summary>普通抽卡费用</summary>
public const int NormalDrawCost = 100;
/// <summary>十连抽费用</summary>
public const int TenDrawCost = 900;
/// <summary>传说英雄概率(2%)</summary>
public const float LegendChance = 0.02f;
/// <summary>史诗英雄概率(10%)</summary>
public const float EpicChance = 0.10f;
/// <summary>稀有英雄概率(35%)</summary>
public const float RareChance = 0.35f;
/// <summary>普通英雄概率(53%)</summary>
public const float CommonChance = 0.53f;
}GDScript
## 咸鱼之王游戏常量
class_name IdleConstants
# ===== 收益 =====
## 基础每秒金币
const BASE_INCOME_PER_SECOND: int = 5
## 每推一关增加的每秒金币
const INCOME_PER_STAGE: int = 2
## 离线收益效率(50%)
const OFFLINE_EFFICIENCY: float = 0.5
## 最大离线时间(秒)——8小时
const MAX_OFFLINE_SECONDS: int = 8 * 3600
## 自动存档间隔(秒)
const AUTO_SAVE_INTERVAL: float = 30.0
# ===== 在线时长奖励 =====
## 在线时长奖励间隔(秒)
const ONLINE_BONUS_INTERVAL: int = 600
## 每次在线时长奖励增加的比例
const ONLINE_BONUS_PER_INTERVAL: float = 0.05
## 在线时长奖励上限
const MAX_ONLINE_BONUS: float = 0.5
# ===== 英雄 =====
## 最大英雄数量
const MAX_HEROES: int = 6
## 初始英雄等级
const HERO_STARTING_LEVEL: int = 1
# ===== 关卡 =====
## 每章关数
const STAGES_PER_CHAPTER: int = 10
# ===== 装备 =====
## 装备最大强化等级
const MAX_ENHANCE_LEVEL: int = 15
# ===== 抽卡 =====
## 普通抽卡费用
const NORMAL_DRAW_COST: int = 100
## 十连抽费用
const TEN_DRAW_COST: int = 900
## 传说概率
const LEGEND_CHANCE: float = 0.02
## 史诗概率
const EPIC_CHANCE: float = 0.10
## 稀有概率
const RARE_CHANCE: float = 0.35
## 普通概率
const COMMON_CHANCE: float = 0.532.4 GameManager——全局管理器
放置游戏的GameManager需要管理:金币、英雄列表、当前关卡、在线时长等数据。
C
using Godot;
using System;
/// <summary>
/// 咸鱼之王全局管理器
/// </summary>
public partial class GameManager : Node
{
// ===== 信号 =====
[Signal] public delegate void GoldChangedEventHandler(int gold);
[Signal] public delegate void IncomeUpdatedEventHandler(float perSecond);
// ===== 核心数据 =====
private int _gold = 0;
private int _diamonds = 0; // 钻石(高级货币)
private int _highestStage = 1; // 推图最高关卡
private int _currentStage = 1; // 当前挑战关卡
private double _totalOnlineSeconds = 0; // 总在线时长
private double _sessionStartTime; // 本次登录时间
// ===== 自动存档 =====
private double _autoSaveTimer = 0;
public int Gold => _gold;
public int Diamonds => _diamonds;
public int HighestStage => _highestStage;
public int CurrentStage => _currentStage;
public override void _Ready()
{
_sessionStartTime = Time.GetTicksMsec() / 1000.0;
// 尝试加载存档
var saveManager = GetNode<SaveManager>("SaveManager");
if (saveManager.HasSave())
{
saveManager.LoadGame();
CalculateOfflineIncome();
}
else
{
// 新游戏:给初始金币
_gold = 500;
}
EmitSignal(SignalName.GoldChanged, _gold);
GD.Print("咸鱼之王 GameManager 已初始化");
}
public override void _Process(double delta)
{
// 累计在线时长
_totalOnlineSeconds += delta;
// 每秒增加金币
float income = IncomeCalculator.CalculateIncomePerSecond(
_highestStage,
HeroBonusMultiplier(),
GetOnlineTimeBonus(),
0f);
int goldGain = (int)(income * delta);
if (goldGain > 0)
{
_gold += goldGain;
EmitSignal(SignalName.GoldChanged, _gold);
}
// 自动存档
_autoSaveTimer += delta;
if (_autoSaveTimer >= IdleConstants.AutoSaveInterval)
{
_autoSaveTimer = 0;
SaveGame();
}
}
/// <summary>
/// 计算离线收益
/// </summary>
private void CalculateOfflineIncome()
{
double lastSaveTime = GetLastSaveTimestamp();
double currentTime = Time.GetTicksMsec() / 1000.0;
double offlineSeconds = currentTime - lastSaveTime;
if (offlineSeconds < 60) return; // 离线不到1分钟,不计算
int offlineGold = IncomeCalculator.CalculateOfflineIncome(
_highestStage, HeroBonusMultiplier(), offlineSeconds);
_gold += offlineGold;
GD.Print($"离线{offlineSeconds / 3600:F1}小时,获得 {offlineGold} 金币");
}
/// <summary>
/// 获取在线时长奖励
/// </summary>
public float GetOnlineTimeBonus()
{
int intervals = (int)(_totalOnlineSeconds / IdleConstants.OnlineBonusInterval);
float bonus = intervals * IdleConstants.OnlineBonusPerInterval;
return Mathf.Min(bonus, IdleConstants.MaxOnlineBonus);
}
/// <summary>
/// 英雄加成倍率
/// </summary>
public float HeroBonusMultiplier()
{
// 简化:所有英雄等级之和 / 100
var heroManager = GetNode<HeroManager>("HeroManager");
int totalLevels = heroManager.GetTotalLevels();
return totalLevels / 100f;
}
/// <summary>
/// 消费金币
/// </summary>
public bool SpendGold(int amount)
{
if (_gold < amount) return false;
_gold -= amount;
EmitSignal(SignalName.GoldChanged, _gold);
return true;
}
/// <summary>
/// 推图成功,更新最高关卡
/// </summary>
public void OnStageCleared(int stageId)
{
if (stageId > _highestStage)
{
_highestStage = stageId;
_currentStage = stageId + 1;
}
}
/// <summary>
/// 手动存档
/// </summary>
public void SaveGame()
{
var saveManager = GetNode<SaveManager>("SaveManager");
saveManager.SaveGame();
}
private double GetLastSaveTimestamp()
{
// 从存档中读取最后保存时间
return Time.GetTicksMsec() / 1000.0 - 3600;
}
}GDScript
extends Node
## 咸鱼之王全局管理器
# ===== 信号 =====
signal gold_changed(gold: int)
signal income_updated(per_second: float)
# ===== 核心数据 =====
var _gold: int = 0
var _diamonds: int = 0
var _highest_stage: int = 1
var _current_stage: int = 1
var _total_online_seconds: float = 0
var _session_start_time: float
var _auto_save_timer: float = 0
var gold: int:
get: return _gold
var diamonds: int:
get: return _diamonds
var highest_stage: int:
get: return _highest_stage
var current_stage: int:
get: return _current_stage
func _ready() -> void:
_session_start_time = Time.get_ticks_msec() / 1000.0
# 尝试加载存档
var save_manager = get_node("SaveManager")
if save_manager.has_save():
save_manager.load_game()
_calculate_offline_income()
else:
_gold = 500
gold_changed.emit(_gold)
print("咸鱼之王 GameManager 已初始化")
func _process(delta: float) -> void:
# 累计在线时长
_total_online_seconds += delta
# 每秒增加金币
var income: float = IncomeCalculator.calculate_income_per_second(
_highest_stage,
_hero_bonus_multiplier(),
_get_online_time_bonus(),
0.0
)
var gold_gain: int = int(income * delta)
if gold_gain > 0:
_gold += gold_gain
gold_changed.emit(_gold)
# 自动存档
_auto_save_timer += delta
if _auto_save_timer >= IdleConstants.AUTO_SAVE_INTERVAL:
_auto_save_timer = 0
save_game()
## 计算离线收益
func _calculate_offline_income() -> void:
var last_save_time: float = _get_last_save_timestamp()
var current_time: float = Time.get_ticks_msec() / 1000.0
var offline_seconds: float = current_time - last_save_time
if offline_seconds < 60:
return
var offline_gold: int = IncomeCalculator.calculate_offline_income(
_highest_stage, _hero_bonus_multiplier(), offline_seconds)
_gold += offline_gold
print("离线%.1f小时,获得 %d 金币" % [offline_seconds / 3600, offline_gold])
## 获取在线时长奖励
func _get_online_time_bonus() -> float:
var intervals: int = int(_total_online_seconds / IdleConstants.ONLINE_BONUS_INTERVAL)
var bonus: float = intervals * IdleConstants.ONLINE_BONUS_PER_INTERVAL
return minf(bonus, IdleConstants.MAX_ONLINE_BONUS)
## 英雄加成倍率
func _hero_bonus_multiplier() -> float:
var hero_manager = get_node("HeroManager")
var total_levels: int = hero_manager.get_total_levels()
return float(total_levels) / 100.0
## 消费金币
func spend_gold(amount: int) -> bool:
if _gold < amount:
return false
_gold -= amount
gold_changed.emit(_gold)
return true
## 推图成功
func on_stage_cleared(stage_id: int) -> void:
if stage_id > _highest_stage:
_highest_stage = stage_id
_current_stage = stage_id + 1
## 手动存档
func save_game() -> void:
var save_manager = get_node("SaveManager")
save_manager.save_game()
func _get_last_save_timestamp() -> float:
return Time.get_ticks_msec() / 1000.0 - 36002.5 自动存档
放置游戏需要频繁自动存档,防止玩家意外关闭丢失进度。同时,每次打开游戏时需要计算离线收益。
存档时机
| 时机 | 说明 |
|---|---|
| 自动存档 | 每30秒自动保存一次 |
| 手动存档 | 玩家在设置中点击保存 |
| 切换到后台 | 应用失去焦点时保存 |
| 关闭游戏 | 应用退出时保存 |
2.6 本章小结
| 内容 | 说明 |
|---|---|
| 项目结构 | 简洁清晰的目录组织 |
| 常量定义 | 收益、英雄、关卡、抽卡等数值常量 |
| GameManager | 金币、关卡、在线时长、自动存档 |
| 自动存档 | 每30秒自动保存,打开时计算离线收益 |
| 离线收益 | 读取上次存档时间,计算差值补发金币 |
下一章我们将实现挂机收益系统——放置游戏的核心。
