I made 2.5D Snake in react-three-fiber
I've made Snake at least 10 times on various hardware and different environments.
One of my first programs was Snake on a TI-83 Graphing Calculator.
You can find the source here.
1<> 2 <Camera fov={45} near={0.005} far={10000} /> 3 <ambientLight intensity={0.5} distance={50} color="white" /> 4 {showAxisHelper && ( 5 <axesHelper 6 size={6} 7 position={[pointLightPosX, pointLightPosY, pointLightPosZ]} 8 /> 9 )} 10 <pointLight 11 intensity={0.5} 12 position={[pointLightPosX, pointLightPosY, pointLightPosZ]} 13 /> 14 <Map 15 width={width} 16 height={height} 17 wireframe={wireframe} 18 effects={mapEffects} 19 onEffectsApplied={remainingMapEffects => 20 setMapEffects(remainingMapEffects) 21 } 22 /> 23 <Snake pieces={snakePieces} wireframe={wireframe} /> 24 {apple && ( 25 <Apple 26 position={[ 27 getPaddedPosition(apple.x), 28 getPaddedPosition(apple.y), 29 getPaddedPosition(apple.z), 30 ]} 31 isGolden={apple.isGolden} 32 wireframe={wireframe} 33 /> 34 )} 35</>
The basic game loop - however components can tie into useFrame
This is Arena.jsx
1useFrame((_, delta) => { 2 if (gamePaused || gameOver) { 3 return; 4 } 5 updateSnakePiecesFromMovement({ delta }); 6 updateSnakePiecesAnimation({ delta }); 7 updateApple(); 8 updateCollision(); 9});
Here are the controls, there's also gesture code in here for mobile.
This is GameHud.jsx
1// Snake controls. 2const handleKeydown = event => { 3 const { keyCode } = event; 4 5 // Up, down, right, left. 6 let newSnakeDirection = snakeDirection; 7 if (keyCode === KEYCODE_UP || keyCode === KEYCODE_W) { 8 newSnakeDirection = 'U'; 9 } else if (keyCode === KEYCODE_DOWN || keyCode === KEYCODE_S) { 10 newSnakeDirection = 'D'; 11 } else if (keyCode === KEYCODE_RIGHT || keyCode === KEYCODE_D) { 12 newSnakeDirection = 'R'; 13 } else if (keyCode === KEYCODE_LEFT || keyCode === KEYCODE_A) { 14 newSnakeDirection = 'L'; 15 } 16 setSnakeDirection(newSnakeDirection); 17 18 // Paused game? 19 if (keyCode === KEYCODE_ESC) { 20 setGamePaused(gamePaused => !gamePaused); 21 } 22 23 // Keyboard shortcuts for game over. 24 if (gameOver) { 25 if (keyCode === KEYCODE_ENTER) { 26 onReplay && onReplay(); 27 } 28 } 29};
Textures in this game are generated based on the theme colors so we can modify them in-game.
This is useCanvasTexture.jsx
1import { useEffect } from 'react'; 2import { CanvasTexture } from 'three'; 3 4let canvasTextures = {}; 5const getCanvasTexture = (color, strokeColor, strokeWidth) => { 6 return canvasTextures[`${color}-${strokeColor}-${strokeWidth}`]; 7}; 8const setCanvasTexture = (color, strokeColor, strokeWidth, texture) => { 9 canvasTextures[`${color}-${strokeColor}-${strokeWidth}`] = texture; 10 return texture; 11}; 12 13export const useCanvasTexture = (color, strokeColor, strokeWidth) => { 14 // Create canvas texture on mount. 15 useEffect(() => { 16 if (getCanvasTexture(color, strokeColor, strokeWidth)) { 17 return; 18 } 19 const canvas = document.createElement('canvas'); 20 canvas.width = 16; 21 canvas.height = 16; 22 23 const ctx = canvas.getContext('2d'); 24 ctx.fillStyle = color; 25 ctx.fillRect(0, 0, canvas.width, canvas.height); 26 ctx.strokeStyle = strokeColor; 27 ctx.lineWidth = strokeWidth; 28 ctx.strokeRect(0, 0, canvas.width, canvas.height); 29 30 setCanvasTexture( 31 color, 32 strokeColor, 33 strokeWidth, 34 new CanvasTexture(canvas), 35 ); 36 }, [color, strokeColor, strokeWidth]); 37 38 return getCanvasTexture(color, strokeColor, strokeWidth); 39};
The map effects of eating an apple.
This is Map.jsx
1// Splash effects. 2const effectSplashes = useRef([]); 3useEffect(() => { 4 if (effects.length <= 0) return; 5 6 for (let effect of effects) { 7 if (effect.type === 'SPLASH') { 8 effectSplashes.current.push({ 9 x_inc: 1, 10 y_inc: 0, 11 z_inc: 0, 12 x: effect.x + 1, 13 y: effect.y, 14 z: effect.z, 15 length: 1, 16 color: themeMapEffectSplashColor, 17 original_x: effect.x + 1, 18 original_y: effect.y, 19 original_z: effect.z, 20 }); 21 effectSplashes.current.push({ 22 x_inc: -1, 23 y_inc: 0, 24 z_inc: 0, 25 x: effect.x - 1, 26 y: effect.y, 27 z: effect.z, 28 length: 1, 29 color: themeMapEffectSplashColor, 30 original_x: effect.x - 1, 31 original_y: effect.y, 32 original_z: effect.z, 33 }); 34 effectSplashes.current.push({ 35 x_inc: 0, 36 y_inc: 0, 37 z_inc: -1, 38 x: effect.x, 39 y: effect.y, 40 z: effect.z - 1, 41 length: 1, 42 color: themeMapEffectSplashColor, 43 original_x: effect.x, 44 original_y: effect.y, 45 original_z: effect.z - 1, 46 }); 47 effectSplashes.current.push({ 48 x_inc: 0, 49 y_inc: 0, 50 z_inc: 1, 51 x: effect.x, 52 y: effect.y, 53 z: effect.z + 1, 54 length: 1, 55 color: themeMapEffectSplashColor, 56 original_x: effect.x, 57 original_y: effect.y, 58 original_z: effect.z + 1, 59 }); 60 } 61 } 62 63 onEffectsApplied([]); 64}, [effects]); 65 66const effectSplashElapsedTime = useRef(0); 67useFrame((_state, delta) => { 68 // Update pieces forward based on game speed. 69 effectSplashElapsedTime.current += delta; 70 if (effectSplashElapsedTime.current <= effectSplashTimeInterval) { 71 return; 72 } 73 effectSplashElapsedTime.current = 0; 74 75 // Iterate through effects. 76 // Find map piece corresponding to effect 77 // NOTE: Should be a clone. 78 let newBlocks = [...blocks]; 79 for (let effectSplash of effectSplashes.current) { 80 let newBlockIndex = 0; 81 for (let block of blocks) { 82 // Apply to blocks vertically. 83 if (effectSplash.x === block.x && effectSplash.z === block.z) { 84 // Apply to all blocks at effect position. 85 newBlocks[newBlockIndex].color = effectSplash.color; 86 } else if ( 87 effectSplash.x + effectSplash.z_inc * effectSplash.length >= 88 block.x && 89 effectSplash.x + effectSplash.z_inc * -1 * effectSplash.length <= 90 block.x && 91 effectSplash.z === block.z 92 ) { 93 // Apply to all blocks in "right" direction given length of effect. 94 newBlocks[newBlockIndex].color = effectSplash.color; 95 } else if ( 96 effectSplash.x - effectSplash.z_inc * effectSplash.length >= 97 block.x && 98 effectSplash.x - effectSplash.z_inc * -1 * effectSplash.length <= 99 block.x && 100 effectSplash.z === block.z 101 ) { 102 // Apply to all blocks in "left" direction given length of effect. 103 newBlocks[newBlockIndex].color = effectSplash.color; 104 } else if ( 105 effectSplash.z - effectSplash.x_inc * effectSplash.length >= 106 block.z && 107 effectSplash.z - effectSplash.x_inc * -1 * effectSplash.length <= 108 block.z && 109 effectSplash.x === block.x 110 ) { 111 // Apply to all blocks in "down" direction given length of effect. 112 newBlocks[newBlockIndex].color = effectSplash.color; 113 } else if ( 114 effectSplash.z + effectSplash.x_inc * effectSplash.length >= 115 block.z && 116 effectSplash.z + effectSplash.x_inc * -1 * effectSplash.length <= 117 block.z && 118 effectSplash.x === block.x 119 ) { 120 // Apply to all blocks in "up" direction given length of effect. 121 newBlocks[newBlockIndex].color = effectSplash.color; 122 } 123 newBlockIndex++; 124 } 125 126 // Clear splash effect color. 127 if ( 128 effectSplash.color === themeMapEffectSplashColor && 129 effectSplash.length === 3 130 ) { 131 effectSplashes.current.push({ 132 x_inc: effectSplash.x_inc, 133 y_inc: effectSplash.y_inc, 134 z_inc: effectSplash.z_inc, 135 x: effectSplash.original_x, 136 y: effectSplash.original_y, 137 z: effectSplash.original_z, 138 length: 1, 139 color: themeMapColor, 140 original_x: effectSplash.original_x, 141 original_y: effectSplash.original_y, 142 original_z: effectSplash.original_z, 143 }); 144 } 145 146 if ( 147 effectSplash.x <= 0 || 148 effectSplash.z <= 0 || 149 effectSplash.x >= width || 150 effectSplash.z >= height 151 ) { 152 // Remove splash effect when it reaches the edges of the map. 153 effectSplash.deleted = true; 154 // Check to see if the effect created a cleanup effect. 155 if ( 156 effectSplash.color === themeMapEffectSplashColor && 157 !effectSplashes.current.find( 158 s => 159 s.color === themeMapColor && 160 s.original_x === effectSplash.original_x && 161 s.original_y === effectSplash.original_y && 162 s.original_z === effectSplash.original_z, 163 ) 164 ) { 165 effectSplashes.current.push({ 166 x_inc: effectSplash.x_inc, 167 y_inc: effectSplash.y_inc, 168 z_inc: effectSplash.z_inc, 169 x: effectSplash.original_x, 170 y: effectSplash.original_y, 171 z: effectSplash.original_z, 172 length: 1, 173 color: themeMapColor, 174 original_x: effectSplash.original_x, 175 original_y: effectSplash.original_y, 176 original_z: effectSplash.original_z, 177 }); 178 } 179 } else { 180 // Apply effect. 181 effectSplash.x += effectSplash.x_inc; 182 effectSplash.y += effectSplash.y_inc; 183 effectSplash.z += effectSplash.z_inc; 184 effectSplash.length += 1; 185 } 186 } 187 setBlocks(newBlocks); 188 effectSplashes.current = effectSplashes.current.filter(e => !e.deleted); 189});
Not the best way to write a game but react-three-fiber
react
canvas
You will fight with state with these abstractions if you're used to mutable state in other rendering engines.