├── .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 | [](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 | 
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 |
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 (

)
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 |
--------------------------------------------------------------------------------