Разработка системы энергии/выносливости 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 механике. Закажите разработку системы энергии под ключ.







