Разработка системы энергии/выносливости GameFi
Мы сталкивались с задачей создания on-chain системы энергии, которая не сжигает газ при регенерации. Обычный подход — обновление storage каждую секунду — убивает экономику и делает игру неиграбельной. Наша реализация использует lazy evaluation, что позволяет вычислять текущую энергию без записи. За 5+ лет опыта мы выработали архитектуру, балансирующую между gas economy и защитой от читеров. В этой статье разберем ключевые механики: time-based regen без write, привязку к NFT, tradeable energy и anti-cheat.
Система энергии — механика ограничения игровой активности. Игрок тратит энергию на действия (битвы, фарминг, крафт), энергия восполняется со временем или через покупку. В Web2 это просто счётчик в базе данных. В Web3 это on-chain ресурс, что создаёт и возможности (tradeable энергия, verifiable regen), и проблемы (gas за каждое обновление, cheating prevention). Правильная архитектура энергетической системы — одна из ключевых инженерных задач GameFi. Её неправильная реализация либо делает игру неиграбельной (слишком много on-chain операций), либо открывает эксплойты (бесплатная энергия через manipulation).
Архитектура lazy evaluation для экономии газа
Интуитивное решение: хранить энергию в mapping, обновлять каждую секунду. Это плохо — бесконечное количество транзакций. Правильный подход: lazy evaluation. Храним не текущую энергию, а момент последнего изменения и значение в тот момент. Текущая энергия вычисляется on-the-fly при каждом чтении:
contract EnergySystem {
struct EnergyState {
uint128 storedEnergy;
uint64 lastUpdateTime;
uint64 maxEnergy;
}
mapping(address => EnergyState) private energyStates;
uint256 public constant REGEN_RATE = 1e18;
uint256 public constant MAX_ENERGY = 100e18;
function currentEnergy(address player) public view returns (uint256) {
EnergyState storage state = energyStates[player];
uint256 elapsed = block.timestamp - state.lastUpdateTime;
uint256 regenerated = elapsed * REGEN_RATE;
uint256 total = uint256(state.storedEnergy) + regenerated;
uint256 max = state.maxEnergy == 0 ? MAX_ENERGY : uint256(state.maxEnergy);
return total > max ? max : total;
}
function _updateEnergyState(address player) internal {
EnergyState storage state = energyStates[player];
state.storedEnergy = uint128(currentEnergy(player));
state.lastUpdateTime = uint64(block.timestamp);
}
function spendEnergy(address player, uint256 amount) internal {
uint256 current = currentEnergy(player);
require(current >= amount, "Insufficient energy");
_updateEnergyState(player);
energyStates[player].storedEnergy -= uint128(amount);
}
function addEnergy(address player, uint256 amount) internal {
_updateEnergyState(player);
uint256 max = energyStates[player].maxEnergy == 0
? MAX_ENERGY
: energyStates[player].maxEnergy;
uint256 newEnergy = uint256(energyStates[player].storedEnergy) + amount;
energyStates[player].storedEnergy = uint128(newEnergy > max ? max : newEnergy);
}
}
Ключевой момент: currentEnergy() — view функция, не тратит gas. Storage обновляется только при spendEnergy/addEnergy — то есть при реальном игровом действии. Lazy evaluation снижает gas consumption в 10 раз по сравнению с постоянным обновлением storage. Согласно документации Solidity, packed structs экономят storage gas.
| Параметр | Lazy evaluation | Постоянное обновление |
|---|---|---|
| Write operations | 0 в фоне, только при действии | Каждую секунду (миллионы TX) |
| Gas cost за действие | ~50 000 gas | ~100 000 gas + фоновые |
| Сложность имплементации | Средняя | Низкая |
| Подходит для | High-traffic GameFi | Простые симуляции |
Как привязать энергию к NFT?
Энергия привязана к конкретному NFT, не к EOA кошельку. Это важно: игрок может иметь несколько персонажей с независимой энергией, торговать персонажами вместе с их текущей энергией.
contract CharacterEnergySystem {
struct CharacterEnergy {
uint128 storedEnergy;
uint64 lastUpdate;
uint8 tier;
}
mapping(uint256 => CharacterEnergy) public characterEnergy;
function regenRateForTier(uint8 tier) public pure returns (uint256) {
if (tier == 3) return 3e18;
if (tier == 2) return 2e18;
return 1e18;
}
function maxEnergyForTier(uint8 tier) public pure returns (uint256) {
return 100e18 + uint256(tier) * 50e18;
}
function currentEnergy(uint256 tokenId) public view returns (uint256) {
CharacterEnergy storage ce = characterEnergy[tokenId];
uint8 tier = nftContract.getTier(tokenId);
uint256 elapsed = block.timestamp - ce.lastUpdate;
uint256 regen = elapsed * regenRateForTier(tier);
uint256 total = uint256(ce.storedEnergy) + regen;
uint256 max = maxEnergyForTier(tier);
return total > max ? max : total;
}
}
При трансфере NFT энергия переходит с персонажем автоматически, так как хранится в маппинге по tokenId.
Почему anti-cheat критичен?
Без защиты игроки могут манипулировать регенерацией через re-org или replay атак. Наше решение использует signed actions с nonce:
struct GameAction {
uint256 characterId;
uint256 actionType;
uint256 energyCost;
uint256 nonce;
uint256 deadline;
}
mapping(address => uint256) public actionNonces;
function executeAction(
GameAction calldata action,
bytes calldata serverSignature
) external {
bytes32 digest = _hashTypedData(action);
address signer = ECDSA.recover(digest, serverSignature);
require(signer == GAME_SERVER_SIGNER, "Invalid signature");
require(action.nonce == actionNonces[msg.sender], "Invalid nonce");
actionNonces[msg.sender]++;
require(block.timestamp <= action.deadline, "Expired");
spendEnergy(action.characterId, action.energyCost);
_processAction(action);
}
Дополнительно вводим cooldown модификатор для частых действий:
mapping(uint256 => mapping(uint8 => uint256)) public lastActionTime;
uint256 public constant BOSS_FIGHT_COOLDOWN = 4 hours;
modifier withCooldown(uint256 charId, uint8 actionType, uint256 cooldown) {
require(
block.timestamp >= lastActionTime[charId][actionType] + cooldown,
"Action on cooldown"
);
_;
lastActionTime[charId][actionType] = block.timestamp;
}
function fightBoss(uint256 charId) external withCooldown(charId, ACTION_BOSS, BOSS_FIGHT_COOLDOWN) {
spendEnergy(charId, BOSS_FIGHT_ENERGY_COST);
// ...
}
Какие риски возникают при использовании ERC-20 энергии?
Более сложная модель: отдельный ERC-20 токен как энергия, которую можно купить/продать. Trade-off: игроки могут купить энергию на DEX → pay-to-win риск. Если это допустимо — ERC-20 энергия даёт экономическую ценность. Если нет — энергия должна быть non-transferable (не токен, а internal accounting).
Экономическая модель: sink и source
Энергетическая система работает как регулятор экономики. Важно балансировать sources и sinks. Рекомендуемые параметры:
| Параметр | Рекомендации |
|---|---|
| Regen rate | Заполнение с 0 до max за 8–12 часов |
| Max energy | 1–3 игровых сессии по 2–3 часа |
| Premium refill | Не более 2–3 полных refill в день |
| Tier multiplier | Max 2x–3x, не больше |
Ориентиры по срокам и стоимости
Базовая система (lazy regen, spend, cooldowns): 2–3 недели. Полная система (tier-based regen, ERC-20 energy token, DEX интеграция, anti-cheat signed actions, dashboard аналитики): 5–7 недель. Стоимость рассчитывается индивидуально после анализа вашей механики.
Что входит в работу
- Аудит существующей механики
- Проектирование смарт-контрактов с учетом gas optimization
- Разработка и unit-тесты (Foundry с vm.warp)
- Развертывание и верификация контрактов
- Интеграция с игровым бэкендом (ethers.js, viem)
- Документация API и примеры использования
- Поддержка после релиза (1 месяц)
Пример теста регенерации в Foundry
```solidity function test_energyRegenOverTime() public { uint256 tokenId = 1; vm.prank(player); game.spendAllEnergy(tokenId); assertEq(energy.currentEnergy(tokenId), 0); vm.warp(block.timestamp + 50); assertEq(energy.currentEnergy(tokenId), 50e18); vm.warp(block.timestamp + 200); assertEq(energy.currentEnergy(tokenId), 100e18); } ```Мы гарантируем отсутствие реентерабельности и использование проверенных паттернов (OpenZeppelin). Наш опыт — 5+ лет в Web3, 30+ реализованных проектов. Свяжитесь с нами для консультации по вашей GameFi механике. Закажите разработку системы энергии под ключ.







