|
| 1 | +This game uses gum session keys for auto approval of transactions. Its ment as a starter game for on chain game. |
| 2 | +There is a js and a unity client for this game. |
| 3 | +Note that neither the program nor session keys are audited. Use at your own risk. |
| 4 | + |
| 5 | +How to run this example: |
| 6 | +Follow the installation here: https://www.anchor-lang.com/docs/installation |
| 7 | +make sure to install solana CLI version 1.14.20 not the 1.16.x version |
| 8 | +sh -c "$(curl -sSfL https://release.solana.com/v1.14.20/install)" |
| 9 | + |
| 10 | +Anchor program |
| 11 | +1. Install the [Anchor CLI](https://project-serum.github.io/anchor/getting-started/installation.html) |
| 12 | +2. `cd lumberjack` `cd program` to end the program directory |
| 13 | +3. Run `anchor build` to build the program |
| 14 | +4. Run `anchor deploy` to deploy the program |
| 15 | +5. Copy the program id from the terminal into the lib.rs, anchor.toml and within the unity project in the LumberjackService and if you use js in the anchor.ts file |
| 16 | +6. Build and deploy again |
| 17 | + |
| 18 | +Next js client |
| 19 | +1. Install [Node.js](https://nodejs.org/en/download/) |
| 20 | +2. Copy the program id into app/utils/anchor.ts |
| 21 | +2. `cd lumberjack` `cd app` to end the app directory |
| 22 | +3. Run `yarn install` to install node modules |
| 23 | +4. Run `yarn dev` to start the client |
| 24 | +5. After doing changes to the anchor program make sure to copy over the types from the program into the client so you can use them |
| 25 | + |
| 26 | +Unity client |
| 27 | +1. Install [Unity](https://unity.com/) |
| 28 | +2. Open the lumberjack scene |
| 29 | +3. Hit play |
| 30 | +4. After doing changes to the anchor program make sure to regenerate the C# client: https://solanacookbook.com/gaming/porting-anchor-to-unity.html#generating-the-client |
| 31 | +Its done like this (after you have build the program): |
| 32 | +cd program |
| 33 | +dotnet tool install Solana.Unity.Anchor.Tool <- run once |
| 34 | +dotnet anchorgen -i target/idl/lumberjack.json -o target/idl/Lumberjack.cs |
| 35 | + |
| 36 | +then copy the c# code into the unity project |
| 37 | + |
| 38 | +To connect to local host from Unity add these links on the wallet holder game object: |
| 39 | +http://localhost:8899 |
| 40 | +ws://localhost:8900 |
| 41 | + |
| 42 | +Here are two videos explaining the energy logic and session keys: |
| 43 | +Session keys: |
| 44 | +https://www.youtube.com/watch?v=oKvWZoybv7Y&t=17s&ab_channel=Solana |
| 45 | +Energy system: |
| 46 | +https://www.youtube.com/watch?v=YYQtRCXJBgs&t=4s&ab_channel=Solana |
| 47 | + |
| 48 | +# Energy System |
| 49 | + |
| 50 | +Many casual games in traditional gaming use energy systems. This is how you can build it on chain. |
| 51 | +Recommended to start with the Solana cookbook [Hello world example]([https://unity.com/](https://solanacookbook.com/gaming/hello-world.html#getting-started-with-your-first-solana-game)). |
| 52 | + |
| 53 | +## Anchor program |
| 54 | + |
| 55 | +Here we will build a program which refills energy over time which the player can then use to perform actions in the game. |
| 56 | +In our example it will be a lumber jack which chops trees. Every tree will reward on wood and cost one energy. |
| 57 | + |
| 58 | +### Creating the player account |
| 59 | + |
| 60 | +First the player needs to create an account which saves the state of our player. Notice the last_login time which will save the current unix time stamp of the player he interacts with the program. |
| 61 | +Like this we will be able to calculate how much energy the player has at a certain point in time. |
| 62 | +We also have a value for wood which will store the wood the lumber jack chucks in the game. |
| 63 | + |
| 64 | +```rust |
| 65 | + |
| 66 | +pub fn init_player(ctx: Context<InitPlayer>) -> Result<()> { |
| 67 | + ctx.accounts.player.energy = MAX_ENERGY; |
| 68 | + ctx.accounts.player.last_login = Clock::get()?.unix_timestamp; |
| 69 | + Ok(()) |
| 70 | +} |
| 71 | + |
| 72 | +... |
| 73 | + |
| 74 | +#[derive(Accounts)] |
| 75 | +pub struct InitPlayer <'info> { |
| 76 | + #[account( |
| 77 | + init, |
| 78 | + payer = signer, |
| 79 | + space = 1000, |
| 80 | + seeds = [b"player".as_ref(), signer.key().as_ref()], |
| 81 | + bump, |
| 82 | + )] |
| 83 | + pub player: Account<'info, PlayerData>, |
| 84 | + #[account(mut)] |
| 85 | + pub signer: Signer<'info>, |
| 86 | + pub system_program: Program<'info, System>, |
| 87 | +} |
| 88 | + |
| 89 | +#[account] |
| 90 | +pub struct PlayerData { |
| 91 | + pub name: String, |
| 92 | + pub level: u8, |
| 93 | + pub xp: u64, |
| 94 | + pub wood: u64, |
| 95 | + pub energy: u64, |
| 96 | + pub last_login: i64 |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +### Choping trees |
| 101 | + |
| 102 | +Then whenever the player calls the chop_tree instruction we will check if the player has enough energy and reward him with one wood. |
| 103 | + |
| 104 | +```rust |
| 105 | + #[error_code] |
| 106 | + pub enum ErrorCode { |
| 107 | + #[msg("Not enough energy")] |
| 108 | + NotEnoughEnergy, |
| 109 | + } |
| 110 | + |
| 111 | + pub fn chop_tree(mut ctx: Context<ChopTree>) -> Result<()> { |
| 112 | + let account = &mut ctx.accounts; |
| 113 | + update_energy(account)?; |
| 114 | + |
| 115 | + if ctx.accounts.player.energy == 0 { |
| 116 | + return err!(ErrorCode::NotEnoughEnergy); |
| 117 | + } |
| 118 | + |
| 119 | + ctx.accounts.player.wood = ctx.accounts.player.wood + 1; |
| 120 | + ctx.accounts.player.energy = ctx.accounts.player.energy - 1; |
| 121 | + msg!("You chopped a tree and got 1 log. You have {} wood and {} energy left.", ctx.accounts.player.wood, ctx.accounts.player.energy); |
| 122 | + Ok(()) |
| 123 | + } |
| 124 | +``` |
| 125 | + |
| 126 | +### Calculating the energy |
| 127 | + |
| 128 | +The interesting part happens in the update_energy function. We check how much time has passed and calculate the energy that the player will have at the given time. |
| 129 | +The same thing we will also do in the client. So we basically lazily update the energy instead of polling it all the time. |
| 130 | +The is a common technic in game development. |
| 131 | + |
| 132 | +```rust |
| 133 | + |
| 134 | +const TIME_TO_REFILL_ENERGY: i64 = 60; |
| 135 | +const MAX_ENERGY: u64 = 10; |
| 136 | + |
| 137 | +pub fn update_energy(ctx: &mut ChopTree) -> Result<()> { |
| 138 | + let mut time_passed: i64 = &Clock::get()?.unix_timestamp - &ctx.player.last_login; |
| 139 | + let mut time_spent: i64 = 0; |
| 140 | + while time_passed > TIME_TO_REFILL_ENERGY { |
| 141 | + ctx.player.energy = ctx.player.energy + 1; |
| 142 | + time_passed -= TIME_TO_REFILL_ENERGY; |
| 143 | + time_spent += TIME_TO_REFILL_ENERGY; |
| 144 | + if ctx.player.energy == MAX_ENERGY { |
| 145 | + break; |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + if ctx.player.energy >= MAX_ENERGY { |
| 150 | + ctx.player.last_login = Clock::get()?.unix_timestamp; |
| 151 | + } else { |
| 152 | + ctx.player.last_login += time_spent; |
| 153 | + } |
| 154 | + |
| 155 | + Ok(()) |
| 156 | +} |
| 157 | +``` |
| 158 | + |
| 159 | +## Js client |
| 160 | + |
| 161 | +### Subscribe to account updates |
| 162 | + |
| 163 | +It is possible to subscribe to account updates via a websocket. This get updates to this account pushed directly back to the client without the need to poll this data. This allows fast gameplay because the updates usually arrive after around 500ms. |
| 164 | + |
| 165 | +```js |
| 166 | +useEffect(() => { |
| 167 | + if (!publicKey) {return;} |
| 168 | + const [pda] = PublicKey.findProgramAddressSync( |
| 169 | + [Buffer.from("player", "utf8"), |
| 170 | + publicKey.toBuffer()], |
| 171 | + new PublicKey(LUMBERJACK_PROGRAM_ID) |
| 172 | + ); |
| 173 | + try { |
| 174 | + program.account.playerData.fetch(pda).then((data) => { |
| 175 | + setGameState(data); |
| 176 | + }); |
| 177 | + } catch (e) { |
| 178 | + window.alert("No player data found, please init!"); |
| 179 | + } |
| 180 | + |
| 181 | + connection.onAccountChange(pda, (account) => { |
| 182 | + setGameState(program.coder.accounts.decode("playerData", account.data)); |
| 183 | + }); |
| 184 | + |
| 185 | + }, [publicKey]); |
| 186 | +``` |
| 187 | + |
| 188 | +### Calculate energy and show countdown |
| 189 | + |
| 190 | +In the java script client we can then perform the same logic and show a countdown timer for the player so that he knows when the next energy will be available: |
| 191 | + |
| 192 | +```js |
| 193 | +useEffect(() => { |
| 194 | + const interval = setInterval(async () => { |
| 195 | + if (gameState == null || gameState.lastLogin == undefined || gameState.energy >= 10) {return;} |
| 196 | + const lastLoginTime=gameState.lastLogin * 1000; |
| 197 | + let timePassed = ((Date.now() - lastLoginTime) / 1000); |
| 198 | + while (timePassed > TIME_TO_REFILL_ENERGY && gameState.energy < MAX_ENERGY) { |
| 199 | + gameState.energy = (parseInt(gameState.energy) + 1); |
| 200 | + gameState.lastLogin = parseInt(gameState.lastLogin) + TIME_TO_REFILL_ENERGY; |
| 201 | + timePassed -= TIME_TO_REFILL_ENERGY; |
| 202 | + } |
| 203 | + setTimePassed(timePassed); |
| 204 | + let nextEnergyIn = Math.floor(TIME_TO_REFILL_ENERGY -timePassed); |
| 205 | + if (nextEnergyIn < TIME_TO_REFILL_ENERGY && nextEnergyIn > 0) { |
| 206 | + setEnergyNextIn(nextEnergyIn); |
| 207 | + } else { |
| 208 | + setEnergyNextIn(0); |
| 209 | + } |
| 210 | + |
| 211 | + }, 1000); |
| 212 | + |
| 213 | + return () => clearInterval(interval); |
| 214 | +}, [gameState, timePassed]); |
| 215 | + |
| 216 | +... |
| 217 | + |
| 218 | +{(gameState && <div className="flex flex-col items-center"> |
| 219 | + {("Wood: " + gameState.wood + " Energy: " + gameState.energy + " Next energy in: " + nextEnergyIn )} |
| 220 | +</div>)} |
| 221 | + |
| 222 | + ``` |
| 223 | + |
| 224 | +### Session keys |
| 225 | + |
| 226 | +Session keys is an optional component. What it does is creating a local key pair which is toped up with some sol which can be used to autoapprove transactions. The session token is only allowed on certain functions of the program and has an expiry of 23 hours. Then the player will get the sol back and can create a new session. |
| 227 | + |
| 228 | +With this you can now build any energy based game and even if someone builds a bot for the game the most he can do is play optimally, which maybe even easier to achieve when playing normally depending on the logic of your game. |
| 229 | + |
| 230 | +This game becomes even better when combined with the Token example from Solana Cookbook and you actually drop some spl token to the players. |
0 commit comments