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







