Unity 开发规划:体力值与资源系统设计方案
在许多游戏中,体力值(Stamina/HP)系统是控制玩家节奏、平衡游戏进度的核心机制,尤其在手游和休闲游戏中应用广泛。本文将详细介绍如何规划和设计一个灵活可扩展的体力值系统,以及相关联的资源恢复、消耗和奖励机制,帮助你在 Unity 项目中构建既平衡又有趣的资源管理体系。
一、体力值系统核心需求分析
一个完善的体力值系统需要考虑以下核心要素:
1. 基础功能需求
体力值的上限与当前值管理
体力消耗机制(如闯关、战斗、采集等行为)
自动恢复机制(时间驱动)
手动恢复机制(道具、付费等)
体力值展示与提示(UI 表现)
2. 扩展功能需求
体力上限提升(通过等级、成就、付费等方式)
恢复速率调整(通过道具、特权等方式)
紧急恢复机制(如观看广告获取临时体力)
体力溢出保护(恢复时超过上限的处理)
离线恢复计算(玩家离线期间的体力恢复)
3. 与其他系统的关联
与任务系统联动(完成任务奖励体力)
与付费系统整合(购买体力或体力恢复道具)
与等级系统挂钩(等级提升时恢复体力)
与社交系统结合(好友赠送体力)
二、系统架构设计
体力值系统应采用模块化设计,确保各部分职责清晰且易于扩展:
1. 核心模块划分
数据模块:管理体力值相关数据与配置
逻辑模块:处理体力计算、恢复、消耗等核心逻辑
UI 模块:负责体力值展示、恢复进度、提示信息
事件模块:处理系统间通信,减少模块耦合
持久化模块:负责体力状态的保存与加载
2. 模块交互流程
plaintext
玩家行为 → 逻辑模块(检查体力)→ 足够 → 消耗体力 → 触发事件 → UI更新 ↓ 不足 → 触发提示 → UI显示恢复选项
三、数据结构设计
合理的数据结构是系统灵活性的基础,建议使用 ScriptableObject 存储配置数据,普通类存储运行时数据。
1. 体力配置数据(ScriptableObject)
csharp
运行
using UnityEngine;[CreateAssetMenu(fileName = "StaminaConfig", menuName = "Game/StaminaConfig")]public class StaminaConfig : ScriptableObject{
[Header("基础设置")]
public int baseMaxStamina = 100; // 基础最大体力值
public float staminaRecoveryInterval = 300f; // 体力恢复间隔(秒)
public int staminaPerRecovery = 1; // 每次恢复的体力值
[Header("等级影响")]
public int staminaPerLevel = 5; // 每级增加的体力值
[Header("限制设置")]
public int maxStaminaCap = 200; // 最大体力上限(防止无限增长)
public bool allowOverfill = true; // 是否允许溢出(如使用道具时)
[Header("恢复道具")]
public StaminaItem[] recoveryItems; // 恢复道具列表}[System.Serializable]public class StaminaItem{
public string itemId; // 道具ID
public int staminaAmount; // 恢复的体力值
public bool isPercentage; // 是否为百分比恢复}2. 体力运行时数据
csharp
运行
using System;[System.Serializable]public class StaminaData{
public int currentStamina; // 当前体力值
public int maxStamina; // 当前最大体力值
public DateTime lastRecoveryTime; // 上次恢复时间
public int pendingRecoveries; // 待恢复的次数(离线计算用)
public int bonusStamina; // 奖励体力(溢出部分)}四、核心逻辑实现规划
1. 体力管理器(StaminaManager)
作为系统核心,负责所有体力相关的逻辑处理:
csharp
运行
using UnityEngine;using System;public class StaminaManager : MonoBehaviour{
public static StaminaManager Instance { get; private set; }
[SerializeField] private StaminaConfig config;
private StaminaData currentData;
private bool isRecovering;
// 事件定义
public event Action<int, int> OnStaminaChanged; // 当前值, 最大值
public event Action OnStaminaFull; // 体力充满时
public event Action<int> OnPendingRecovery; // 有可恢复体力时
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
Initialize();
}
else
{
Destroy(gameObject);
}
}
// 初始化体力数据
private void Initialize()
{
// 从持久化系统加载数据
currentData = DataPersistence.Instance.LoadStaminaData();
// 如果是新玩家,初始化数据
if (currentData == null)
{
currentData = new StaminaData
{
currentStamina = config.baseMaxStamina,
maxStamina = config.baseMaxStamina,
lastRecoveryTime = DateTime.Now };
}
else
{
// 计算离线期间应恢复的体力
CalculateOfflineRecovery();
}
// 启动恢复计时器
StartRecoveryTimer();
}
// 计算离线恢复的体力
private void CalculateOfflineRecovery()
{
// 获取当前时间(考虑时区问题)
DateTime now = DateTime.Now;
TimeSpan timeSinceLast = now - currentData.lastRecoveryTime;
// 计算可恢复次数
int possibleRecoveries = (int)(timeSinceLast.TotalSeconds / config.staminaRecoveryInterval);
int maxPossible = config.baseMaxStamina - currentData.currentStamina;
// 实际恢复次数(不超过最大可能)
int actualRecoveries = Mathf.Min(possibleRecoveries, maxPossible);
// 更新体力值
currentData.currentStamina += actualRecoveries * config.staminaPerRecovery;
currentData.lastRecoveryTime = currentData.lastRecoveryTime.AddSeconds(
actualRecoveries * config.staminaRecoveryInterval );
// 保存数据
SaveStaminaData();
}
// 启动恢复计时器
private void StartRecoveryTimer()
{
if (!isRecovering)
{
isRecovering = true;
StartCoroutine(RecoveryCoroutine());
}
}
// 恢复协程
private System.Collections.IEnumerator RecoveryCoroutine()
{
while (true)
{
// 计算距离下次恢复的时间
TimeSpan timeToNext = currentData.lastRecoveryTime
+ TimeSpan.FromSeconds(config.staminaRecoveryInterval)
- DateTime.Now;
// 如果时间已到,立即恢复
if (timeToNext.TotalSeconds <= 0)
{
RecoverStamina();
timeToNext = TimeSpan.FromSeconds(config.staminaRecoveryInterval);
}
// 通知UI下次恢复时间
OnPendingRecovery?.Invoke((int)timeToNext.TotalSeconds);
// 等待到下次恢复
yield return new WaitForSeconds((float)timeToNext.TotalSeconds);
}
}
// 恢复一次体力
private void RecoverStamina()
{
// 检查是否已达最大值
if (currentData.currentStamina >= currentData.maxStamina)
{
OnStaminaFull?.Invoke();
return;
}
// 增加体力
currentData.currentStamina += config.staminaPerRecovery;
currentData.lastRecoveryTime = DateTime.Now;
// 确保不超过最大值
if (currentData.currentStamina > currentData.maxStamina)
{
currentData.currentStamina = currentData.maxStamina;
}
// 触发事件
OnStaminaChanged?.Invoke(currentData.currentStamina, currentData.maxStamina);
// 保存数据
SaveStaminaData();
}
// 消耗体力
public bool ConsumeStamina(int amount)
{
if (amount <= 0) return false;
// 检查是否有足够体力
if (currentData.currentStamina >= amount)
{
currentData.currentStamina -= amount;
OnStaminaChanged?.Invoke(currentData.currentStamina, currentData.maxStamina);
SaveStaminaData();
return true;
}
// 检查是否有奖励体力可以使用
if (currentData.bonusStamina > 0)
{
int useBonus = Mathf.Min(amount - currentData.currentStamina, currentData.bonusStamina);
currentData.currentStamina = 0;
currentData.bonusStamina -= useBonus;
if (currentData.currentStamina + currentData.bonusStamina >= amount)
{
currentData.bonusStamina -= (amount - currentData.currentStamina);
currentData.currentStamina = 0;
OnStaminaChanged?.Invoke(currentData.currentStamina, currentData.maxStamina);
SaveStaminaData();
return true;
}
}
// 体力不足
return false;
}
// 增加体力(使用道具等)
public void AddStamina(int amount, bool ignoreCap = false)
{
if (amount <= 0) return;
// 检查是否忽略上限(如特殊奖励)
if (ignoreCap || config.allowOverfill)
{
// 先填充基础体力
int neededToFill = currentData.maxStamina - currentData.currentStamina;
if (neededToFill > 0)
{
int fillAmount = Mathf.Min(amount, neededToFill);
currentData.currentStamina += fillAmount;
amount -= fillAmount;
}
// 剩余部分作为奖励体力(溢出)
if (amount > 0)
{
currentData.bonusStamina += amount;
}
}
else
{
// 不允许溢出,直接增加不超过上限
currentData.currentStamina = Mathf.Min(
currentData.currentStamina + amount,
currentData.maxStamina );
}
OnStaminaChanged?.Invoke(currentData.currentStamina, currentData.maxStamina);
SaveStaminaData();
}
// 提升最大体力值
public void IncreaseMaxStamina(int amount)
{
if (amount <= 0) return;
// 计算新的最大值(不超过上限)
int newMax = currentData.maxStamina + amount;
currentData.maxStamina = Mathf.Min(newMax, config.maxStaminaCap);
OnStaminaChanged?.Invoke(currentData.currentStamina, currentData.maxStamina);
SaveStaminaData();
}
// 根据等级更新最大体力
public void UpdateMaxStaminaByLevel(int level)
{
int calculatedMax = config.baseMaxStamina + (level - 1) * config.staminaPerLevel;
currentData.maxStamina = Mathf.Min(calculatedMax, config.maxStaminaCap);
OnStaminaChanged?.Invoke(currentData.currentStamina, currentData.maxStamina);
SaveStaminaData();
}
// 保存体力数据
private void SaveStaminaData()
{
DataPersistence.Instance.SaveStaminaData(currentData);
}
// 外部获取当前体力状态
public (int current, int max, int bonus) GetStaminaStatus()
{
return (currentData.currentStamina, currentData.maxStamina, currentData.bonusStamina);
}}2. 数据持久化处理
体力数据需要在游戏重启后保持,实现一个简单的数据持久化类:
csharp
运行
using UnityEngine;using System;public class DataPersistence : MonoBehaviour{
public static DataPersistence Instance { get; private set; }
private const string StaminaDataKey = "StaminaData";
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
// 保存体力数据
public void SaveStaminaData(StaminaData data)
{
// 注意:DateTime序列化需要特殊处理
var serializableData = new SerializableStaminaData(data);
string json = JsonUtility.ToJson(serializableData);
PlayerPrefs.SetString(StaminaDataKey, json);
PlayerPrefs.Save();
}
// 加载体力数据
public StaminaData LoadStaminaData()
{
if (PlayerPrefs.HasKey(StaminaDataKey))
{
string json = PlayerPrefs.GetString(StaminaDataKey);
var serializableData = JsonUtility.FromJson<SerializableStaminaData>(json);
return serializableData.ToStaminaData();
}
return null;
}}// 可序列化的体力数据(处理DateTime)[System.Serializable]public class SerializableStaminaData{
public int currentStamina;
public int maxStamina;
public string lastRecoveryTime; // 存储为字符串
public int pendingRecoveries;
public int bonusStamina;
public SerializableStaminaData(StaminaData data)
{
currentStamina = data.currentStamina;
maxStamina = data.maxStamina;
lastRecoveryTime = data.lastRecoveryTime.ToString("o"); // ISO 8601格式
pendingRecoveries = data.pendingRecoveries;
bonusStamina = data.bonusStamina;
}
public StaminaData ToStaminaData()
{
return new StaminaData
{
currentStamina = currentStamina,
maxStamina = maxStamina,
lastRecoveryTime = DateTime.Parse(lastRecoveryTime),
pendingRecoveries = pendingRecoveries,
bonusStamina = bonusStamina };
}}五、UI 界面设计规划
体力系统的 UI 需要清晰展示当前状态并提供必要的交互入口:
1. UI 组件构成
体力值显示条(进度条或数字)
恢复倒计时(显示下次恢复时间)
恢复按钮(使用道具或付费)
体力不足提示面板
奖励体力特殊标识
2. UI 逻辑实现
csharp
运行
using UnityEngine;using UnityEngine.UI;using TMPro;public class StaminaUI : MonoBehaviour{
[Header("体力显示")]
[SerializeField] private Slider staminaSlider;
[SerializeField] private TextMeshProUGUI staminaText;
[SerializeField] private TextMeshProUGUI bonusStaminaText;
[Header("恢复信息")]
[SerializeField] private TextMeshProUGUI recoveryTimeText;
[SerializeField] private Button recoverButton;
[Header("提示面板")]
[SerializeField] private GameObject insufficientPanel;
private void OnEnable()
{
// 注册事件监听
StaminaManager.Instance.OnStaminaChanged += UpdateStaminaDisplay;
StaminaManager.Instance.OnPendingRecovery += UpdateRecoveryTime;
StaminaManager.Instance.OnStaminaFull += HideRecoveryTime;
// 初始化显示
var status = StaminaManager.Instance.GetStaminaStatus();
UpdateStaminaDisplay(status.current, status.max);
}
private void OnDisable()
{
// 取消事件监听
if (StaminaManager.Instance != null)
{
StaminaManager.Instance.OnStaminaChanged -= UpdateStaminaDisplay;
StaminaManager.Instance.OnPendingRecovery -= UpdateRecoveryTime;
StaminaManager.Instance.OnStaminaFull -= HideRecoveryTime;
}
}
private void Start()
{
// 绑定按钮事件
recoverButton.onClick.AddListener(OnRecoverButtonClicked);
// 隐藏提示面板
insufficientPanel.SetActive(false);
}
// 更新体力显示
private void UpdateStaminaDisplay(int current, int max)
{
staminaSlider.maxValue = max;
staminaSlider.value = current;
staminaText.text = $"{current}/{max}";
// 显示奖励体力(如果有)
var status = StaminaManager.Instance.GetStaminaStatus();
bonusStaminaText.gameObject.SetActive(status.bonus > 0);
if (status.bonus > 0)
{
bonusStaminaText.text = $"+{status.bonus}";
}
}
// 更新恢复时间显示
private void UpdateRecoveryTime(int seconds)
{
recoveryTimeText.gameObject.SetActive(true);
int minutes = seconds / 60;
int secs = seconds % 60;
recoveryTimeText.text = $"下次恢复: {minutes:00}:{secs:00}";
}
// 隐藏恢复时间(体力已满)
private void HideRecoveryTime()
{
recoveryTimeText.gameObject.SetActive(false);
recoveryTimeText.text = "";
}
// 恢复按钮点击
private void OnRecoverButtonClicked()
{
// 这里可以打开恢复选项面板
// 如使用道具、观看广告、付费购买等
}
// 显示体力不足提示
public void ShowInsufficientWarning()
{
insufficientPanel.SetActive(true);
}
// 关闭提示面板
public void CloseInsufficientWarning()
{
insufficientPanel.SetActive(false);
}}六、系统扩展规划
1. 体力恢复道具系统
实现可消耗道具恢复体力:
csharp
运行
public class StaminaItemManager : MonoBehaviour{
// 使用体力恢复道具
public bool UseStaminaItem(string itemId)
{
// 检查玩家是否拥有该道具
if (!InventoryManager.Instance.HasItem(itemId))
return false;
// 查找道具配置
var config = StaminaManager.Instance.Config.recoveryItems.
FirstOrDefault(i => i.itemId == itemId);
if (config == null)
return false;
// 计算恢复量
int recoverAmount = config.isPercentage
? (int)(StaminaManager.Instance.GetMaxStamina() * config.staminaAmount / 100f)
: config.staminaAmount;
// 消耗道具并恢复体力
if (InventoryManager.Instance.ConsumeItem(itemId, 1))
{
StaminaManager.Instance.AddStamina(recoverAmount);
return true;
}
return false;
}}2. 广告恢复体力功能
集成广告 SDK 实现免费恢复体力:
csharp
运行
public class AdRewardStamina : MonoBehaviour{
public void ShowStaminaAd()
{
// 显示激励广告
AdManager.Instance.ShowRewardedAd(
onComplete: () =>
{
// 广告完成后奖励体力
StaminaManager.Instance.AddStamina(20);
UIManager.Instance.ShowNotification("获得20点体力!");
},
onFailed: () =>
{
UIManager.Instance.ShowNotification("广告加载失败,请稍后再试");
}
);
}}3. 每日体力奖励系统
结合每日任务提供体力奖励:
csharp
运行
public class DailyRewardSystem : MonoBehaviour{
// 领取每日体力奖励
public void ClaimDailyStamina()
{
if (CanClaimDailyStamina())
{
int rewardAmount = 50 + (PlayerLevel.Instance.Level * 2); // 随等级提升
StaminaManager.Instance.AddStamina(rewardAmount);
MarkDailyStaminaClaimed();
}
}
// 检查是否可以领取
private bool CanClaimDailyStamina()
{
// 实现每日领取逻辑
return true;
}
// 标记为已领取
private void MarkDailyStaminaClaimed()
{
// 记录领取状态
}}七、平衡设计与优化建议
1. 数值平衡要点
体力恢复速度应与游戏核心玩法节奏匹配(如每 5 分钟恢复 1 点,适合 10-15 分钟 / 局的游戏)
最大体力值应能支持玩家 1-2 小时的连续游戏(避免频繁中断)
付费恢复的性价比需谨慎设计,避免破坏免费玩家体验
随玩家等级提升适当增加体力上限,延长单次游戏时间
2. 性能优化
体力恢复计时器使用协程而非 Update 方法
离线计算仅在游戏启动时执行一次,避免频繁计算
UI 更新通过事件驱动,而非每帧刷新
对于移动端,注意 DateTime 的时区问题(建议使用 UTC 时间)
3. 玩家体验优化
体力即将耗尽时给予提前提示
提供 "一键恢复" 快捷操作
体力充满时发送通知提醒玩家
特殊节日可临时提高体力恢复速度或上限
八、总结
体力值系统作为控制游戏节奏的核心机制,其设计需要兼顾游戏平衡、商业化需求和玩家体验。一个好的体力系统应该让玩家感受到适度的挑战和约束,同时提供多种灵活的恢复途径。
本文提供的设计方案采用模块化架构,便于根据项目需求进行扩展和调整。在实际开发中,建议先搭建基础框架,通过 playtest 不断调整数值参数,找到最适合你游戏的体力系统平衡点。
希望这份规划方案能为你的 Unity 项目提供有价值的参考,祝开发顺利!