7. 回合系统与经济
2026/4/14大约 8 分钟
回合系统与经济
回合制是 CS 区别于其他射击游戏的标志性特征。每一回合都是一个独立的"小战役"——有开始(买枪)、有过程(战斗)、有结局(胜负判定),回合之间的经济传承又让每回合不是孤立的。这一章我们实现回合管理器和经济系统。
回合管理器
回合管理器是整个游戏的"裁判"——它控制回合的流程、计时、胜负判定,以及半场换边。
回合流程
比赛开始
│
├── 上半场(最多12回合,先到13分结束)
│ │
│ ├─① 购买阶段(15秒)─── 买枪时间
│ │ │
│ ├─② 战斗阶段(115秒)── 正式战斗
│ │ ├─ T 全灭 → CT 赢
│ │ ├─ CT 全灭 → T 赢
│ │ ├─ 炸弹爆炸 → T 赢
│ │ ├─ 炸弹拆除 → CT 赢
│ │ └─ 时间耗尽且炸弹未安放 → CT 赢
│ │
│ ├─③ 回合结算(5秒)
│ │ ├─ 发放金钱奖励
│ │ ├─ 更新比分
│ │ └─ 重生所有角色
│ │
│ └─ 回到①
│
├── 半场休息(15秒)── 双方换边
│
└── 下半场(最多12回合)回合管理器实现
C#
// Scripts/Systems/RoundManager.cs
using Godot;
public enum RoundPhase
{
BuyPhase, // 购买阶段
PlayPhase, // 战斗阶段
RoundEnd, // 回合结算
HalfTime, // 半场休息
MatchOver // 比赛结束
}
/// <summary>
/// 回合管理器。控制回合流程、计时和胜负判定。
/// </summary>
public partial class RoundManager : Node
{
[Export] public float BuyPhaseDuration { get; set; } = 15f;
[Export] public float PlayPhaseDuration { get; set; } = 115f;
[Export] public float RoundEndDuration { get; set; } = 5f;
[Export] public float HalfTimeDuration { get; set; } = 15f;
[Export] public int WinsNeeded { get; set; } = 13;
private RoundPhase _phase;
private float _phaseTimer;
private int _ctScore;
private int _tScore;
private int _roundNumber;
private bool _bombPlanted;
private bool _isFirstHalf = true;
// 回合内状态追踪
private int _aliveCT;
private int _aliveT;
public override void _Ready()
{
var gameEvents = GetNode<GameEvents>("/root/GameEvents");
gameEvents.PlayerDied += OnPlayerDied;
gameEvents.BombPlanted += OnBombPlanted;
gameEvents.BombDefused += OnBombDefused;
gameEvents.BombExploded += OnBombExploded;
}
/// <summary>
/// 开始新回合。
/// </summary>
public void StartRound()
{
_roundNumber++;
_bombPlanted = false;
_phase = RoundPhase.BuyPhase;
_phaseTimer = BuyPhaseDuration;
// 统计存活人数
_aliveCT = GetTree().GetNodesInGroup("counter_terrorist").Count;
_aliveT = GetTree().GetNodesInGroup("terrorist").Count;
var gameEvents = GetNode<GameEvents>("/root/GameEvents");
gameEvents.EmitSignal(GameEvents.SignalName.RoundStarted, _roundNumber);
}
public override void _Process(double delta)
{
if (_phase == RoundPhase.MatchOver) return;
_phaseTimer -= (float)delta;
switch (_phase)
{
case RoundPhase.BuyPhase:
if (_phaseTimer <= 0)
{
_phase = RoundPhase.PlayPhase;
_phaseTimer = PlayPhaseDuration;
}
break;
case RoundPhase.PlayPhase:
// 时间耗尽
if (_phaseTimer <= 0 && !_bombPlanted)
{
// CT 赢(T 没能在时间内安放炸弹)
EndRound("CT");
}
break;
case RoundPhase.RoundEnd:
if (_phaseTimer <= 0)
{
// 检查是否需要换边
if (_roundNumber >= 12 && _isFirstHalf)
{
_phase = RoundPhase.HalfTime;
_phaseTimer = HalfTimeDuration;
}
else if (_ctScore >= WinsNeeded || _tScore >= WinsNeeded)
{
_phase = RoundPhase.MatchOver;
}
else
{
StartRound();
}
}
break;
case RoundPhase.HalfTime:
if (_phaseTimer <= 0)
{
_isFirstHalf = false;
// 交换双方阵营和分数
(_ctScore, _tScore) = (_tScore, _ctScore);
StartRound();
}
break;
}
}
private void EndRound(string winner)
{
_phase = RoundPhase.RoundEnd;
_phaseTimer = RoundEndDuration;
if (winner == "CT") _ctScore++;
else _tScore++;
var gameEvents = GetNode<GameEvents>("/root/GameEvents");
gameEvents.EmitSignal(GameEvents.SignalName.RoundEnded, winner);
// 通知经济系统
var economy = GetNode<EconomyManager>("/root/EconomyManager");
economy.ProcessRoundEnd(winner);
}
// 事件处理
private void OnPlayerDied(Node3D player, Node3D killer)
{
if (player.IsInGroup("counter_terrorist")) _aliveCT--;
else if (player.IsInGroup("terrorist")) _aliveT--;
// 检查是否有一方全灭
if (_phase == RoundPhase.PlayPhase)
{
if (_aliveCT <= 0) EndRound("T");
else if (_aliveT <= 0) EndRound("CT");
}
}
private void OnBombPlanted(Vector3 position)
{
_bombPlanted = true;
}
private void OnBombDefused()
{
EndRound("CT");
}
private void OnBombExploded()
{
EndRound("T");
}
// 公开查询接口
public RoundPhase GetPhase() => _phase;
public float GetPhaseTimeRemaining() => _phaseTimer;
public (int ct, int t) GetScore() => (_ctScore, _tScore);
public int GetRoundNumber() => _roundNumber;
}GDScript
# Scripts/Systems/RoundManager.gd
extends Node
enum RoundPhase {
BUY_PHASE, # 购买阶段
PLAY_PHASE, # 战斗阶段
ROUND_END, # 回合结算
HALF_TIME, # 半场休息
MATCH_OVER # 比赛结束
}
@export var buy_phase_duration: float = 15.0
@export var play_phase_duration: float = 115.0
@export var round_end_duration: float = 5.0
@export var half_time_duration: float = 15.0
@export var wins_needed: int = 13
var _phase: RoundPhase
var _phase_timer: float = 0.0
var _ct_score: int = 0
var _t_score: int = 0
var _round_number: int = 0
var _bomb_planted: bool = false
var _is_first_half: bool = true
# 回合内状态追踪
var _alive_ct: int = 0
var _alive_t: int = 0
func _ready():
var game_events = get_node("/root/GameEvents") as GameEvents
game_events.player_died.connect(_on_player_died)
game_events.bomb_planted.connect(_on_bomb_planted)
game_events.bomb_defused.connect(_on_bomb_defused)
game_events.bomb_exploded.connect(_on_bomb_exploded)
## 开始新回合。
func start_round():
_round_number += 1
_bomb_planted = false
_phase = RoundPhase.BUY_PHASE
_phase_timer = buy_phase_duration
# 统计存活人数
_alive_ct = get_tree().get_nodes_in_group("counter_terrorist").size()
_alive_t = get_tree().get_nodes_in_group("terrorist").size()
var game_events = get_node("/root/GameEvents") as GameEvents
game_events.round_started.emit(_round_number)
func _process(delta: float):
if _phase == RoundPhase.MATCH_OVER:
return
_phase_timer -= delta
match _phase:
RoundPhase.BUY_PHASE:
if _phase_timer <= 0:
_phase = RoundPhase.PLAY_PHASE
_phase_timer = play_phase_duration
RoundPhase.PLAY_PHASE:
# 时间耗尽
if _phase_timer <= 0 and not _bomb_planted:
_end_round("CT")
RoundPhase.ROUND_END:
if _phase_timer <= 0:
# 检查是否需要换边
if _round_number >= 12 and _is_first_half:
_phase = RoundPhase.HALF_TIME
_phase_timer = half_time_duration
elif _ct_score >= wins_needed or _t_score >= wins_needed:
_phase = RoundPhase.MATCH_OVER
else:
start_round()
RoundPhase.HALF_TIME:
if _phase_timer <= 0:
_is_first_half = false
# 交换双方阵营和分数
var temp = _ct_score
_ct_score = _t_score
_t_score = temp
start_round()
func _end_round(winner: String):
_phase = RoundPhase.ROUND_END
_phase_timer = round_end_duration
if winner == "CT":
_ct_score += 1
else:
_t_score += 1
var game_events = get_node("/root/GameEvents") as GameEvents
game_events.round_ended.emit(winner)
# 通知经济系统
var economy = get_node("/root/EconomyManager") as EconomyManager
economy.process_round_end(winner)
# 事件处理
func _on_player_died(player: Node3D, _killer: Node3D):
if player.is_in_group("counter_terrorist"):
_alive_ct -= 1
elif player.is_in_group("terrorist"):
_alive_t -= 1
if _phase == RoundPhase.PLAY_PHASE:
if _alive_ct <= 0:
_end_round("T")
elif _alive_t <= 0:
_end_round("CT")
func _on_bomb_planted(_position: Vector3):
_bomb_planted = true
func _on_bomb_defused():
_end_round("CT")
func _on_bomb_exploded():
_end_round("T")
# 公开查询接口
func get_phase() -> RoundPhase:
return _phase
func get_phase_time_remaining() -> float:
return _phase_timer
func get_score() -> Dictionary:
return {"ct": _ct_score, "t": _t_score}
func get_round_number() -> int:
return _round_number经济系统
经济系统是 CS 的另一个标志性机制——每一回合赚到的钱可以用来买下一回合的武器,让每回合之间存在"传承"关系。
金钱规则
C#
// Scripts/Systems/EconomyManager.cs
using Godot;
/// <summary>
/// 经济管理器。追踪每个玩家的金钱,处理回合结算时的金钱发放。
///
/// CS 经济系统的核心规则:
/// - 赢了回合 → 每人得 $3250
/// - 输了回合 → 基础 $1400,连续输递增
/// - 击杀奖励:步枪 $300,霰弹枪 $900,AWP $100
/// - 安放炸弹:$300(无论输赢)
/// </summary>
public partial class EconomyManager : Node
{
private int _consecutiveLosses;
private const int MaxMoney = 16000;
// 每个玩家的金钱(用 playerId 作为 key)
private Godot.Collections.Dictionary<int, int> _playerMoney = new();
public override void _Ready()
{
var gameEvents = GetNode<GameEvents>("/root/GameEvents");
gameEvents.WeaponPurchased += OnWeaponPurchased;
}
/// <summary>
/// 初始化一个玩家的金钱(开局 $800)
/// </summary>
public void InitPlayer(int playerId)
{
_playerMoney[playerId] = 800; // CS 的初始金钱
}
/// <summary>
/// 回合结束时调用。根据胜负发放金钱。
/// </summary>
public void ProcessRoundEnd(string winningTeam)
{
// 计算胜方和败方的奖金
int winReward = 3250;
int lossReward = CalculateLossReward();
if (winningTeam == "CT")
{
_consecutiveLosses = 0; // CT 赢了,CT 的连败清零
}
else
{
_consecutiveLosses++; // CT 输了,连败计数+1
}
// 给所有玩家发钱
foreach (var (playerId, currentMoney) in _playerMoney)
{
// 判断这个玩家属于哪方(简化处理)
int reward = IsCTPlayer(playerId) ?
(winningTeam == "CT" ? winReward : lossReward) :
(winningTeam == "T" ? winReward : lossReward);
_playerMoney[playerId] = Mathf.Min(currentMoney + reward, MaxMoney);
}
// 通知 UI 更新
EmitMoneyChanged();
}
/// <summary>
/// 计算连败递增奖励。
/// 连续输的次数越多,每回合的安慰金越高。
/// </summary>
private int CalculateLossReward()
{
return _consecutiveLosses switch
{
0 => 1400,
1 => 1900,
2 => 2400,
_ => 2900 // 连续输 3 次及以上都是 2900
};
}
/// <summary>
/// 玩家购买武器。
/// </summary>
public bool TryPurchase(int playerId, int cost)
{
if (!_playerMoney.ContainsKey(playerId)) return false;
var currentMoney = _playerMoney[playerId];
if (currentMoney < cost) return false;
_playerMoney[playerId] = currentMoney - cost;
var gameEvents = GetNode<GameEvents>("/root/GameEvents");
gameEvents.EmitSignal(GameEvents.SignalName.MoneyChanged, playerId, _playerMoney[playerId]);
return true;
}
/// <summary>
/// 击杀奖励。
/// </summary>
public void AwardKillReward(int playerId, string weaponType)
{
int reward = weaponType switch
{
"Rifle" => 300,
"Pistol" => 300,
"Shotgun" => 900,
"Sniper" => 100,
"SMG" => 600,
_ => 300
};
if (_playerMoney.ContainsKey(playerId))
{
_playerMoney[playerId] = Mathf.Min(
_playerMoney[playerId] + reward,
MaxMoney
);
}
}
private void OnWeaponPurchased(int playerId, string weaponName)
{
// UI 发来的购买请求会在这里处理
}
private bool IsCTPlayer(int playerId)
{
// 简化处理,实际应根据玩家数据判断
return playerId % 2 == 0;
}
public int GetMoney(int playerId)
{
return _playerMoney.GetValueOrDefault(playerId, 0);
}
private void EmitMoneyChanged()
{
var gameEvents = GetNode<GameEvents>("/root/GameEvents");
foreach (var (playerId, money) in _playerMoney)
{
gameEvents.EmitSignal(GameEvents.SignalName.MoneyChanged, playerId, money);
}
}
}GDScript
# Scripts/Systems/EconomyManager.gd
extends Node
## 经济管理器。追踪每个玩家的金钱,处理回合结算时的金钱发放。
var _consecutive_losses: int = 0
const MAX_MONEY: int = 16000
# 每个玩家的金钱
var _player_money: Dictionary = {}
func _ready():
var game_events = get_node("/root/GameEvents") as GameEvents
game_events.weapon_purchased.connect(_on_weapon_purchased)
## 初始化一个玩家的金钱(开局 $800)
func init_player(player_id: int):
_player_money[player_id] = 800
## 回合结束时调用。根据胜负发放金钱。
func process_round_end(winning_team: String):
var win_reward: int = 3250
var loss_reward: int = _calculate_loss_reward()
if winning_team == "CT":
_consecutive_losses = 0
else:
_consecutive_losses += 1
# 给所有玩家发钱
for player_id in _player_money:
var current_money: int = _player_money[player_id]
var is_ct: bool = _is_ct_player(player_id)
var reward: int
if is_ct:
reward = win_reward if winning_team == "CT" else loss_reward
else:
reward = win_reward if winning_team == "T" else loss_reward
_player_money[player_id] = mini(current_money + reward, MAX_MONEY)
_emit_money_changed()
## 计算连败递增奖励。
func _calculate_loss_reward() -> int:
match _consecutive_losses:
0: return 1400
1: return 1900
2: return 2400
_: return 2900 # 连续输 3 次及以上
## 玩家购买武器。
func try_purchase(player_id: int, cost: int) -> bool:
if not _player_money.has(player_id):
return false
var current_money: int = _player_money[player_id]
if current_money < cost:
return false
_player_money[player_id] = current_money - cost
var game_events = get_node("/root/GameEvents") as GameEvents
game_events.money_changed.emit(player_id, _player_money[player_id])
return true
## 击杀奖励。
func award_kill_reward(player_id: int, weapon_type: String):
var reward: int
match weapon_type:
"Rifle": reward = 300
"Pistol": reward = 300
"Shotgun": reward = 900
"Sniper": reward = 100
"SMG": reward = 600
_: reward = 300
if _player_money.has(player_id):
_player_money[player_id] = mini(
_player_money[player_id] + reward,
MAX_MONEY
)
func _on_weapon_purchased(_player_id: int, _weapon_name: String):
pass
func _is_ct_player(player_id: int) -> bool:
return player_id % 2 == 0
func get_money(player_id: int) -> int:
return _player_money.get(player_id, 0)
func _emit_money_changed():
var game_events = get_node("/root/GameEvents") as GameEvents
for player_id in _player_money:
game_events.money_changed.emit(player_id, _player_money[player_id])买枪菜单
买枪菜单只在购买阶段显示,提供一个简单的界面让玩家选择武器。
C#
// Scripts/UI/BuyMenuController.cs
using Godot;
/// <summary>
/// 买枪菜单控制器。在购买阶段按 B 键打开。
///
/// 显示可购买的武器列表和价格,
/// 点击购买后扣钱、装备武器、关闭菜单。
/// </summary>
public partial class BuyMenuController : CanvasLayer
{
private bool _isVisible;
private int _playerId;
public override void _Ready()
{
Visible = false;
}
public override void _UnhandledInput(InputEvent @event)
{
if (@event.IsActionPressed("buy_menu"))
{
var roundManager = GetParent().GetNode<RoundManager>("RoundManager");
if (roundManager.GetPhase() == RoundPhase.BuyPhase)
{
ToggleMenu();
}
}
}
private void ToggleMenu()
{
_isVisible = !_isVisible;
Visible = _isVisible;
if (_isVisible)
{
Input.MouseMode = Input.MouseModeEnum.Visible;
}
else
{
Input.MouseMode = Input.MouseModeEnum.Captured;
}
}
/// <summary>
/// 购买武器(由 UI 按钮调用)
/// </summary>
public void OnPurchaseButtonPressed(string weaponName, int cost)
{
var economy = GetNode<EconomyManager>("/root/EconomyManager");
if (economy.TryPurchase(_playerId, cost))
{
var gameEvents = GetNode<GameEvents>("/root/GameEvents");
gameEvents.EmitSignal(GameEvents.SignalName.WeaponPurchased, _playerId, weaponName);
// 关闭菜单
ToggleMenu();
}
}
}GDScript
# Scripts/UI/BuyMenuController.gd
extends CanvasLayer
## 买枪菜单控制器。在购买阶段按 B 键打开。
var _is_visible: bool = false
var _player_id: int = 0
func _ready():
visible = false
func _unhandled_input(event: InputEvent):
if event.is_action_pressed("buy_menu"):
var round_manager = get_parent().get_node("RoundManager") as RoundManager
if round_manager.get_phase() == RoundManager.RoundPhase.BUY_PHASE:
_toggle_menu()
func _toggle_menu():
_is_visible = not _is_visible
visible = _is_visible
if _is_visible:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
else:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
## 购买武器(由 UI 按钮调用)
func on_purchase_button_pressed(weapon_name: String, cost: int):
var economy = get_node("/root/EconomyManager") as EconomyManager
if economy.try_purchase(_player_id, cost):
var game_events = get_node("/root/GameEvents") as GameEvents
game_events.weapon_purchased.emit(_player_id, weapon_name)
_toggle_menu()小结
这一章实现了 CS 的回合系统与经济:
- 回合管理器:控制购买/战斗/结算三个阶段,判定六种胜负条件
- 经济系统:回合奖金、连败递增、击杀奖励、最大金钱上限
- 买枪菜单:购买阶段按 B 打开,花钱买武器
关键设计:
- 用事件总线(信号)解耦回合管理器和其他系统
- 连败递增机制让落后方有机会翻盘
- 金钱上限($16000)防止一方无限积累财富
下一章我们实现 HUD 界面和炸弹系统。
