├── .gitignore ├── README.md ├── TUTORIAL.md ├── package-lock.json ├── package.json ├── preview.png ├── public ├── favicon.ico └── index.html └── src ├── App.js ├── components ├── Cube.js ├── Cubes.js ├── FPV.js ├── Ground.js ├── Menu.js ├── Player.js └── TextureSelector.js ├── hooks ├── useKeyboard.js └── useStore.js ├── images ├── dirt.jpg ├── glass.png ├── grass.jpg ├── images.js ├── log.jpg ├── textures.js └── wood.png ├── index.css └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to React three fiber Minecraft 2 | 3 | This project is just me trying to mimic Minecraft in React. 4 | How i did it can be seen in this Youtube video: 5 | [![Video preview](https://img.youtube.com/vi/qpOZup_3P_A/0.jpg)](https://www.youtube.com/watch?v=qpOZup_3P_A) 6 | 7 | Demo: https://minecraft-freecodecamp.vercel.app/ 8 | 9 | ## How to play? 10 | 11 | Currently it has 5 types of blocks: Grass, Wood, Log, Glass and Dirt. 12 | You switch blocks with numbers 1-5 on your keyboard. 13 | You navigate the world with the mouse and WASD. 14 | You can click to add blocks and Alt+Click to remove blocks. 15 | You world is stored in your browsers local storage. 16 | 17 | ![Preview](preview.png 'Preview') 18 | 19 | ## Want to extend, develop modify? 20 | 21 | If you do so or just build a cool world. Please share it with me dbark@hey.com or https://twitter.com/barelydaniel 22 | 23 | In the project directory, you can run: 24 | 25 | ### `npm install && npm start` 26 | 27 | Runs the app in the development mode.
28 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 29 | 30 | The page will reload if you make edits.
31 | You will also see any lint errors in the console. 32 | 33 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 34 | -------------------------------------------------------------------------------- /TUTORIAL.md: -------------------------------------------------------------------------------- 1 | - [x] Boilerplate 2 | - [x] Sky 3 | - [x] Textures and images 4 | - [x] Ground 5 | - [x] Keyboard inputs 6 | - [x] Player 7 | - [x] First person view 8 | - [x] Gravity 9 | - [x] Movement 10 | - [x] State management 11 | - [x] Cubes 12 | - [x] Adding cubes 13 | - [x] Removing cubes 14 | - [x] Cube type selector 15 | - [x] Save world in localstorage 16 | - [x] Hover state on cubes 17 | - [x] Build a house 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minecraft-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@react-three/cannon": "^3.1.2", 7 | "@react-three/drei": "^7.12.5", 8 | "@react-three/fiber": "^7.0.7", 9 | "react": "^17.0.2", 10 | "react-dom": "^17.0.2", 11 | "react-scripts": "4.0.3", 12 | "three": "^0.132.2", 13 | "nanoid": "^3.1.20", 14 | "zustand": "^3.0.3" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danba340/minecraft-freecodecamp/6bf493c23022ee997440e56f09eba87f1f7c4d5b/preview.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danba340/minecraft-freecodecamp/6bf493c23022ee997440e56f09eba87f1f7c4d5b/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | React Minecraft 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { Physics } from '@react-three/cannon'; 2 | import { Sky } from '@react-three/drei'; 3 | import { Canvas } from '@react-three/fiber'; 4 | import { Ground } from './components/Ground' 5 | import { Player } from './components/Player' 6 | import { FPV } from './components/FPV' 7 | import { Cubes } from './components/Cubes' 8 | import { TextureSelector } from './components/TextureSelector'; 9 | import { Menu } from './components/Menu'; 10 | 11 | function App() { 12 | return ( 13 | <> 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
+
25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /src/components/Cube.js: -------------------------------------------------------------------------------- 1 | import { useBox } from "@react-three/cannon" 2 | import { useState } from "react" 3 | import { useStore } from "../hooks/useStore" 4 | import * as textures from "../images/textures" 5 | 6 | 7 | export const Cube = ({ position, texture }) => { 8 | const [isHovered, setIsHovered] = useState(false) 9 | const [ref] = useBox(() => ({ 10 | type: 'Static', 11 | position 12 | })) 13 | const [addCube, removeCube] = useStore((state) => [state.addCube, state.removeCube]) 14 | 15 | const activeTexture = textures[texture + 'Texture'] 16 | 17 | 18 | 19 | return ( 20 | { 22 | e.stopPropagation() 23 | setIsHovered(true) 24 | }} 25 | onPointerOut={(e) => { 26 | e.stopPropagation() 27 | setIsHovered(false) 28 | }} 29 | onClick={(e) => { 30 | e.stopPropagation() 31 | const clickedFace = Math.floor(e.faceIndex / 2) 32 | const { x, y, z } = ref.current.position 33 | if (e.altKey) { 34 | removeCube(x, y, z) 35 | return 36 | } 37 | else if (clickedFace === 0) { 38 | addCube(x + 1, y, z) 39 | return 40 | } 41 | else if (clickedFace === 1) { 42 | addCube(x - 1, y, z) 43 | return 44 | } 45 | else if (clickedFace === 2) { 46 | addCube(x, y + 1, z) 47 | return 48 | } 49 | else if (clickedFace === 3) { 50 | addCube(x, y - 1, z) 51 | return 52 | } 53 | else if (clickedFace === 4) { 54 | addCube(x, y, z + 1) 55 | return 56 | } 57 | else if (clickedFace === 5) { 58 | addCube(x, y, z - 1) 59 | return 60 | } 61 | }} 62 | ref={ref} 63 | > 64 | 65 | 71 | 72 | ) 73 | } -------------------------------------------------------------------------------- /src/components/Cubes.js: -------------------------------------------------------------------------------- 1 | import { useStore } from '../hooks/useStore' 2 | import { Cube } from './Cube' 3 | 4 | export const Cubes = () => { 5 | const [cubes] = useStore((state) => [ 6 | state.cubes 7 | ]) 8 | return cubes.map(({ key, pos, texture }) => { 9 | return ( 10 | 11 | ) 12 | }) 13 | } -------------------------------------------------------------------------------- /src/components/FPV.js: -------------------------------------------------------------------------------- 1 | import { PointerLockControls } from "@react-three/drei" 2 | import { useThree } from "@react-three/fiber" 3 | 4 | export const FPV = () => { 5 | const { camera, gl } = useThree() 6 | 7 | return () 8 | } -------------------------------------------------------------------------------- /src/components/Ground.js: -------------------------------------------------------------------------------- 1 | import { usePlane } from "@react-three/cannon" 2 | import { groundTexture } from "../images/textures" 3 | import { useStore } from '../hooks/useStore' 4 | 5 | export const Ground = () => { 6 | const [ref] = usePlane(() => ({ 7 | rotation: [-Math.PI / 2, 0, 0], position: [0, -0.5, 0] 8 | })) 9 | const [addCube] = useStore((state) => [state.addCube]) 10 | 11 | 12 | groundTexture.repeat.set(100, 100) 13 | 14 | return ( 15 | { 17 | e.stopPropagation() 18 | const [x, y, z] = Object.values(e.point).map(val => Math.ceil(val)); 19 | addCube(x, y, z) 20 | }} 21 | ref={ref} 22 | > 23 | 24 | 25 | 26 | ) 27 | } -------------------------------------------------------------------------------- /src/components/Menu.js: -------------------------------------------------------------------------------- 1 | import { useStore } from "../hooks/useStore" 2 | 3 | export const Menu = () => { 4 | const [saveWorld, resetWorld] = useStore((state) => [state.saveWorld, state.resetWorld]) 5 | 6 | return (
7 | 10 | 13 |
) 14 | } -------------------------------------------------------------------------------- /src/components/Player.js: -------------------------------------------------------------------------------- 1 | import { useFrame, useThree } from "@react-three/fiber" 2 | import { useSphere } from "@react-three/cannon" 3 | import { useEffect, useRef } from "react" 4 | import { Vector3 } from "three" 5 | import { useKeyboard } from "../hooks/useKeyboard" 6 | 7 | const JUMP_FORCE = 4; 8 | const SPEED = 4; 9 | 10 | export const Player = () => { 11 | const { moveBackward, moveForward, moveRight, moveLeft, jump } = useKeyboard() 12 | 13 | const { camera } = useThree() 14 | const [ref, api] = useSphere(() => ({ 15 | mass: 1, 16 | type: 'Dynamic', 17 | position: [0, 1, 0], 18 | })) 19 | 20 | const vel = useRef([0, 0, 0]) 21 | useEffect(() => { 22 | api.velocity.subscribe((v) => vel.current = v) 23 | }, [api.velocity]) 24 | 25 | const pos = useRef([0, 0, 0]) 26 | useEffect(() => { 27 | api.position.subscribe((p) => pos.current = p) 28 | }, [api.position]) 29 | 30 | useFrame(() => { 31 | camera.position.copy(new Vector3(pos.current[0], pos.current[1], pos.current[2])) 32 | 33 | const direction = new Vector3() 34 | 35 | const frontVector = new Vector3( 36 | 0, 37 | 0, 38 | (moveBackward ? 1 : 0) - (moveForward ? 1 : 0) 39 | ) 40 | 41 | const sideVector = new Vector3( 42 | (moveLeft ? 1 : 0) - (moveRight ? 1 : 0), 43 | 0, 44 | 0, 45 | ) 46 | 47 | direction 48 | .subVectors(frontVector, sideVector) 49 | .normalize() 50 | .multiplyScalar(SPEED) 51 | .applyEuler(camera.rotation) 52 | 53 | api.velocity.set(direction.x, vel.current[1], direction.z) 54 | 55 | if (jump && Math.abs(vel.current[1]) < 0.05) { 56 | api.velocity.set(vel.current[0], JUMP_FORCE, vel.current[2]) 57 | } 58 | }) 59 | 60 | return ( 61 | 62 | ) 63 | } -------------------------------------------------------------------------------- /src/components/TextureSelector.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { useStore } from "../hooks/useStore" 3 | import { useKeyboard } from "../hooks/useKeyboard" 4 | import { dirtImg, grassImg, glassImg, logImg, woodImg } from '../images/images' 5 | 6 | const images = { 7 | dirt: dirtImg, 8 | grass: grassImg, 9 | glass: glassImg, 10 | wood: woodImg, 11 | log: logImg, 12 | } 13 | 14 | export const TextureSelector = () => { 15 | const [visible, setVisible] = useState(false) 16 | const [activeTexture, setTexture] = useStore((state) => [state.texture, state.setTexture]) 17 | const { 18 | dirt, 19 | grass, 20 | glass, 21 | wood, 22 | log, 23 | } = useKeyboard() 24 | 25 | useEffect(() => { 26 | const textures = { 27 | dirt, 28 | grass, 29 | glass, 30 | wood, 31 | log 32 | } 33 | const pressedTexture = Object.entries(textures).find(([k, v]) => v) 34 | if (pressedTexture) { 35 | setTexture(pressedTexture[0]) 36 | } 37 | }, [setTexture, dirt, grass, glass, wood, log]) 38 | 39 | 40 | 41 | useEffect(() => { 42 | const visibilityTimeout = setTimeout(() => { 43 | setVisible(false) 44 | }, 2000) 45 | setVisible(true) 46 | return () => { 47 | clearTimeout(visibilityTimeout) 48 | } 49 | }, [activeTexture]) 50 | 51 | return visible && ( 52 |
53 | {Object.entries(images).map(([k, src]) => { 54 | return ({k}) 60 | })} 61 |
62 | ) 63 | } -------------------------------------------------------------------------------- /src/hooks/useKeyboard.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react" 2 | 3 | function actionByKey(key) { 4 | const keyActionMap = { 5 | KeyW: 'moveForward', 6 | KeyS: 'moveBackward', 7 | KeyA: 'moveLeft', 8 | KeyD: 'moveRight', 9 | Space: 'jump', 10 | Digit1: 'dirt', 11 | Digit2: 'grass', 12 | Digit3: 'glass', 13 | Digit4: 'wood', 14 | Digit5: 'log', 15 | } 16 | return keyActionMap[key] 17 | } 18 | 19 | export const useKeyboard = () => { 20 | const [actions, setActions] = useState({ 21 | moveForward: false, 22 | moveBackward: false, 23 | moveLeft: false, 24 | moveRight: false, 25 | jump: false, 26 | dirt: false, 27 | grass: false, 28 | glass: false, 29 | wood: false, 30 | log: false, 31 | }) 32 | 33 | const handleKeyDown = useCallback((e) => { 34 | const action = actionByKey(e.code) 35 | if (action) { 36 | setActions((prev) => { 37 | return ({ 38 | ...prev, 39 | [action]: true 40 | }) 41 | }) 42 | } 43 | }, []) 44 | 45 | const handleKeyUp = useCallback((e) => { 46 | const action = actionByKey(e.code) 47 | if (action) { 48 | setActions((prev) => { 49 | return ({ 50 | ...prev, 51 | [action]: false 52 | }) 53 | }) 54 | } 55 | }, []) 56 | 57 | useEffect(() => { 58 | document.addEventListener('keydown', handleKeyDown) 59 | document.addEventListener('keyup', handleKeyUp) 60 | return () => { 61 | document.removeEventListener('keydown', handleKeyDown) 62 | document.removeEventListener('keyup', handleKeyUp) 63 | } 64 | }, [handleKeyDown, handleKeyUp]) 65 | 66 | return actions 67 | } -------------------------------------------------------------------------------- /src/hooks/useStore.js: -------------------------------------------------------------------------------- 1 | import create from 'zustand' 2 | import { nanoid } from 'nanoid' 3 | 4 | const getLocalStorage = (key) => JSON.parse(window.localStorage.getItem(key)) 5 | const setLocalStorage = (key, value) => window.localStorage.setItem(key, JSON.stringify(value)) 6 | 7 | 8 | export const useStore = create((set) => ({ 9 | texture: 'dirt', 10 | cubes: getLocalStorage('cubes') || [], 11 | addCube: (x, y, z) => { 12 | set((prev) => ({ 13 | cubes: [ 14 | ...prev.cubes, 15 | { 16 | key: nanoid(), 17 | pos: [x, y, z], 18 | texture: prev.texture 19 | } 20 | ] 21 | })) 22 | }, 23 | removeCube: (x, y, z) => { 24 | set((prev) => ({ 25 | cubes: prev.cubes.filter(cube => { 26 | const [X, Y, Z] = cube.pos 27 | return X !== x || Y !== y || Z !== z 28 | }) 29 | 30 | })) 31 | }, 32 | setTexture: (texture) => { 33 | set(() => ({ 34 | texture 35 | })) 36 | }, 37 | saveWorld: () => { 38 | set((prev) => { 39 | setLocalStorage('cubes', prev.cubes) 40 | }) 41 | }, 42 | resetWorld: () => { 43 | set(() => ({ 44 | cubes: [] 45 | })) 46 | }, 47 | })) -------------------------------------------------------------------------------- /src/images/dirt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danba340/minecraft-freecodecamp/6bf493c23022ee997440e56f09eba87f1f7c4d5b/src/images/dirt.jpg -------------------------------------------------------------------------------- /src/images/glass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danba340/minecraft-freecodecamp/6bf493c23022ee997440e56f09eba87f1f7c4d5b/src/images/glass.png -------------------------------------------------------------------------------- /src/images/grass.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danba340/minecraft-freecodecamp/6bf493c23022ee997440e56f09eba87f1f7c4d5b/src/images/grass.jpg -------------------------------------------------------------------------------- /src/images/images.js: -------------------------------------------------------------------------------- 1 | import dirtImg from './dirt.jpg'; 2 | import grassImg from './grass.jpg'; 3 | import glassImg from './glass.png'; 4 | import logImg from './log.jpg'; 5 | import woodImg from './wood.png'; 6 | 7 | export { 8 | dirtImg, 9 | grassImg, 10 | glassImg, 11 | woodImg, 12 | logImg, 13 | } 14 | -------------------------------------------------------------------------------- /src/images/log.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danba340/minecraft-freecodecamp/6bf493c23022ee997440e56f09eba87f1f7c4d5b/src/images/log.jpg -------------------------------------------------------------------------------- /src/images/textures.js: -------------------------------------------------------------------------------- 1 | import { NearestFilter, TextureLoader, RepeatWrapping } from 'three' 2 | 3 | import { 4 | dirtImg, 5 | logImg, 6 | grassImg, 7 | glassImg, 8 | woodImg 9 | } from './images' 10 | 11 | const dirtTexture = new TextureLoader().load(dirtImg) 12 | const logTexture = new TextureLoader().load(logImg) 13 | const grassTexture = new TextureLoader().load(grassImg) 14 | const glassTexture = new TextureLoader().load(glassImg) 15 | const woodTexture = new TextureLoader().load(woodImg) 16 | const groundTexture = new TextureLoader().load(grassImg) 17 | 18 | dirtTexture.magFilter = NearestFilter; 19 | logTexture.magFilter = NearestFilter; 20 | grassTexture.magFilter = NearestFilter; 21 | glassTexture.magFilter = NearestFilter; 22 | woodTexture.magFilter = NearestFilter; 23 | groundTexture.magFilter = NearestFilter; 24 | groundTexture.wrapS = RepeatWrapping 25 | groundTexture.wrapT = RepeatWrapping 26 | 27 | export { 28 | dirtTexture, 29 | logTexture, 30 | grassTexture, 31 | glassTexture, 32 | woodTexture, 33 | groundTexture 34 | } -------------------------------------------------------------------------------- /src/images/wood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danba340/minecraft-freecodecamp/6bf493c23022ee997440e56f09eba87f1f7c4d5b/src/images/wood.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body, 7 | #root { 8 | width: 100%; 9 | height: 100%; 10 | margin: 0; 11 | padding: 0; 12 | background-color: lightblue; 13 | -webkit-touch-callout: none; 14 | -webkit-user-select: none; 15 | -khtml-user-select: none; 16 | -moz-user-select: none; 17 | -ms-user-select: none; 18 | user-select: none; 19 | overflow: hidden; 20 | } 21 | 22 | body { 23 | position: fixed; 24 | overflow: hidden; 25 | overscroll-behavior-y: none; 26 | font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, ubuntu, roboto, noto, segoe ui, arial, sans-serif; 27 | font-size: 40px; 28 | } 29 | 30 | .flex { 31 | display: flex; 32 | } 33 | 34 | .fixed { 35 | position: fixed; 36 | } 37 | 38 | .relative { 39 | position: relative; 40 | } 41 | 42 | .absolute { 43 | position: absolute; 44 | } 45 | 46 | .centered { 47 | top: 50%; 48 | left: 50%; 49 | transform: translate(-50%, -50%); 50 | } 51 | .cursor { 52 | color: white; 53 | } 54 | 55 | .texture-selector { 56 | transform: scale(5); 57 | } 58 | 59 | .texture-selector img.active { 60 | border: 2px solid red; 61 | } 62 | 63 | .menu { 64 | top: 0px; 65 | left: 10px; 66 | } 67 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root'), 11 | ); 12 | --------------------------------------------------------------------------------