├── .gitignore
├── README.md
├── index.html
├── package.json
├── public
└── vite.svg
├── src
├── App.jsx
├── assets
│ └── react.svg
├── components
│ ├── Cube.jsx
│ ├── Cubes.jsx
│ ├── FPV.jsx
│ ├── Ground.jsx
│ ├── Player.jsx
│ └── TextureSelect.jsx
├── hooks
│ ├── useKeyboard.js
│ └── useStore.js
├── images
│ ├── dirt.jpg
│ ├── glass.png
│ ├── grass.jpg
│ ├── images.js
│ ├── log.jpg
│ ├── textures.js
│ └── wood.png
├── index.css
└── main.jsx
└── vite.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Minecraft Clone
2 | Clon de [Minecraft](https://minecraft.net/) hecho con
3 |
4 | - [React](https://reactjs.org/)
5 | - [ThreeJS](https://threejs.org/) (Libreria de 3D para JS)
6 | - [@react-three/cannon](https://cannon.pmnd.rs/)
7 | - [@react-three/drei](https://drei.pmnd.rs/)
8 | - [@react-three/fiber](https://docs.pmnd.rs/react-three-fiber/)
9 | - [ViteJS](https://vitejs.dev) (Empaquetador)
10 |
11 | Basado en el vídeo de [FreecodeCamp](https://youtube.com/watch?v=qpOZup_3P_A&t=0s)
12 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "minecraft-clone",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "@react-three/cannon": "6.5.0",
13 | "@react-three/drei": "9.40.0",
14 | "@react-three/fiber": "8.9.1",
15 | "nanoid": "4.0.0",
16 | "react": "18.2.0",
17 | "react-dom": "18.2.0",
18 | "three": "0.146.0",
19 | "zustand": "4.1.4"
20 | },
21 | "devDependencies": {
22 | "@types/react": "18.0.24",
23 | "@types/react-dom": "18.0.8",
24 | "@vitejs/plugin-react": "2.2.0",
25 | "standard": "17.0.0",
26 | "vite": "3.2.3"
27 | },
28 | "eslintConfig": {
29 | "extends": [
30 | "./node_modules/standard/eslintrc.json"
31 | ]
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { Canvas } from '@react-three/fiber'
2 | import { Sky } from '@react-three/drei'
3 | import { Physics } from '@react-three/cannon'
4 | import { Ground } from './components/Ground.jsx'
5 | import { FPV as Fpv } from './components/FPV.jsx'
6 | import { Player } from './components/Player.jsx'
7 | import { Cubes } from './components/Cubes.jsx'
8 | import { TextureSelector } from './components/TextureSelect.jsx'
9 |
10 | function App () {
11 | return (
12 | <>
13 |
24 | +
25 |
26 | >
27 | )
28 | }
29 |
30 | export default App
31 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Cube.jsx:
--------------------------------------------------------------------------------
1 | import { useStore } from '../hooks/useStore.js'
2 | import { useBox } from '@react-three/cannon'
3 | import { useState } from 'react'
4 | import * as textures from '../images/textures.js'
5 |
6 | export const Cube = ({ id, position, texture }) => {
7 | const [isHovered, setIsHovered] = useState(false)
8 | const [removeCube] = useStore(state => [state.removeCube])
9 |
10 | const [ref] = useBox(() => ({
11 | type: 'Static',
12 | position
13 | }))
14 |
15 | const activeTexture = textures[texture + 'Texture']
16 |
17 | return (
18 | {
20 | e.stopPropagation()
21 | setIsHovered(true)
22 | }}
23 | onPointerOut={(e) => {
24 | e.stopPropagation()
25 | setIsHovered(false)
26 | }}
27 | ref={ref}
28 | onClick={(e) => {
29 | e.stopPropagation()
30 |
31 | if (e.altKey) {
32 | removeCube(id)
33 | }
34 | }}
35 | >
36 |
37 |
43 |
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/Cubes.jsx:
--------------------------------------------------------------------------------
1 | import { useStore } from '../hooks/useStore'
2 | import { Cube } from './Cube.jsx'
3 |
4 | export const Cubes = () => {
5 | const [cubes] = useStore(state => [state.cubes])
6 |
7 | return cubes.map(({ id, pos, texture }) => {
8 | return (
9 |
15 | )
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/FPV.jsx:
--------------------------------------------------------------------------------
1 | import { PointerLockControls } from '@react-three/drei'
2 | import { useThree } from '@react-three/fiber'
3 |
4 | export function FPV () {
5 | const { camera, gl } = useThree()
6 |
7 | return (
8 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/Ground.jsx:
--------------------------------------------------------------------------------
1 | import { usePlane } from '@react-three/cannon'
2 | import { useStore } from '../hooks/useStore.js'
3 | import { groundTexture } from '../images/textures.js'
4 |
5 | export function Ground () {
6 | const [ref] = usePlane(() => ({
7 | rotation: [-Math.PI / 2, 0, 0],
8 | position: [0, -0.5, 0]
9 | }))
10 |
11 | const [addCube] = useStore(state => [state.addCube])
12 |
13 | groundTexture.repeat.set(100, 100)
14 |
15 | const handleClickGround = event => {
16 | event.stopPropagation()
17 | const [x, y, z] = Object.values(event.point)
18 | .map(n => Math.ceil(n))
19 |
20 | addCube(x, y, z)
21 | }
22 |
23 | return (
24 |
28 |
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/Player.jsx:
--------------------------------------------------------------------------------
1 | import { useSphere } from '@react-three/cannon'
2 | import { useFrame, useThree } from '@react-three/fiber'
3 | import { useEffect, useRef } from 'react'
4 | import { Vector3 } from 'three'
5 | import { useKeyboard } from '../hooks/useKeyboard.js'
6 |
7 | const CHARACTER_SPEED = 4
8 | const CHARACTER_JUMP_FORCE = 4
9 |
10 | export const Player = () => {
11 | const {
12 | moveBackward,
13 | moveForward,
14 | moveLeft,
15 | moveRight,
16 | jump
17 | } = useKeyboard()
18 |
19 | const { camera } = useThree()
20 | const [ref, api] = useSphere(() => ({
21 | mass: 1,
22 | type: 'Dynamic',
23 | position: [0, 0.5, 0]
24 | }))
25 |
26 | const pos = useRef([0, 0, 0])
27 | useEffect(() => {
28 | api.position.subscribe(p => {
29 | pos.current = p
30 | })
31 | }, [api.position])
32 |
33 | const vel = useRef([0, 0, 0])
34 | useEffect(() => {
35 | api.velocity.subscribe(p => {
36 | vel.current = p
37 | })
38 | }, [api.velocity])
39 |
40 | useFrame(() => {
41 | camera.position.copy(
42 | new Vector3(
43 | pos.current[0], // x
44 | pos.current[1], // y
45 | pos.current[2] // z
46 | )
47 | )
48 |
49 | const direction = new Vector3()
50 |
51 | const frontVector = new Vector3(
52 | 0,
53 | 0,
54 | (moveBackward ? 1 : 0) - (moveForward ? 1 : 0)
55 | )
56 |
57 | const sideVector = new Vector3(
58 | (moveLeft ? 1 : 0) - (moveRight ? 1 : 0),
59 | 0,
60 | 0
61 | )
62 |
63 | direction
64 | .subVectors(frontVector, sideVector)
65 | .normalize()
66 | .multiplyScalar(CHARACTER_SPEED) // walk: 2, run: 5
67 | .applyEuler(camera.rotation)
68 |
69 | api.velocity.set(
70 | direction.x,
71 | vel.current[1], // ???? saltar.
72 | direction.z
73 | )
74 |
75 | if (jump && Math.abs(vel.current[1]) < 0.05) {
76 | api.velocity.set(
77 | vel.current[0],
78 | CHARACTER_JUMP_FORCE,
79 | vel.current[2]
80 | )
81 | }
82 | })
83 |
84 | return (
85 |
86 | )
87 | }
88 |
--------------------------------------------------------------------------------
/src/components/TextureSelect.jsx:
--------------------------------------------------------------------------------
1 | import { useStore } from '../hooks/useStore.js'
2 | import * as images from '../images/images.js'
3 | import { useKeyboard } from '../hooks/useKeyboard.js'
4 | import { useEffect, useState } from 'react'
5 |
6 | export const TextureSelector = () => {
7 | const [visible, setVisible] = useState(true)
8 | const [texture, setTexture] = useStore(state => [state.texture, state.setTexture])
9 |
10 | const {
11 | dirt,
12 | grass,
13 | glass,
14 | wood,
15 | log
16 | } = useKeyboard()
17 |
18 | useEffect(() => {
19 | const visibilityTimeout = setTimeout(() => {
20 | setVisible(false)
21 | }, 1000)
22 |
23 | setVisible(true)
24 |
25 | return () => {
26 | clearTimeout(visibilityTimeout)
27 | }
28 | }, [texture])
29 |
30 | useEffect(() => {
31 | const options = {
32 | dirt,
33 | grass,
34 | glass,
35 | wood,
36 | log
37 | }
38 |
39 | const selectedTexture = Object
40 | .entries(options)
41 | .find(([texture, isEnabled]) => isEnabled)
42 |
43 | if (selectedTexture) {
44 | const [textureName] = selectedTexture
45 | setTexture(textureName)
46 | }
47 | }, [dirt, grass, glass, wood, log])
48 |
49 | return (
50 |
51 | {
52 | Object
53 | .entries(images)
54 | .map(([imgKey, img]) => {
55 | return (
56 |

62 | )
63 | })
64 | }
65 |
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/src/hooks/useKeyboard.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | const ACTIONS_KEYBOARD_MAP = {
4 | KeyW: 'moveForward',
5 | KeyS: 'moveBackward',
6 | KeyA: 'moveLeft',
7 | KeyD: 'moveRight',
8 | Space: 'jump',
9 | Digit1: 'dirt',
10 | Digit2: 'grass',
11 | Digit3: 'glass',
12 | Digit4: 'wood',
13 | Digit5: 'log'
14 | }
15 |
16 | export const useKeyboard = () => {
17 | const [actions, setActions] = useState({
18 | moveForward: false,
19 | moveBackward: false,
20 | moveLeft: false,
21 | moveRight: false,
22 | jump: false,
23 | dirt: false,
24 | grass: false,
25 | glass: false,
26 | wood: false,
27 | log: false
28 | })
29 |
30 | useEffect(() => {
31 | const handleKeyDown = event => {
32 | const { code } = event
33 | const action = ACTIONS_KEYBOARD_MAP[code]
34 |
35 | if (action) {
36 | // if (actions[action]) return
37 |
38 | setActions(prevActions => ({
39 | ...prevActions,
40 | [action]: true
41 | }))
42 | }
43 | }
44 |
45 | const handleKeyUp = event => {
46 | const { code } = event
47 | const action = ACTIONS_KEYBOARD_MAP[code]
48 |
49 | if (action) {
50 | // if (!actions[action]) return
51 |
52 | setActions(prevActions => ({
53 | ...prevActions,
54 | [action]: false
55 | }))
56 | }
57 | }
58 |
59 | document.addEventListener('keydown', handleKeyDown)
60 | document.addEventListener('keyup', handleKeyUp)
61 |
62 | return () => {
63 | document.removeEventListener('keydown', handleKeyDown)
64 | document.removeEventListener('keyup', handleKeyUp)
65 | }
66 | }, [])
67 |
68 | return actions
69 | }
70 |
--------------------------------------------------------------------------------
/src/hooks/useStore.js:
--------------------------------------------------------------------------------
1 | import { nanoid } from 'nanoid'
2 | import create from 'zustand'
3 |
4 | export const useStore = create(set => ({
5 | texture: 'dirt',
6 | cubes: [{
7 | id: nanoid(),
8 | pos: [1, 1, 1],
9 | texture: 'dirt'
10 | }, {
11 | id: nanoid(),
12 | pos: [1, 5, 1],
13 | texture: 'log'
14 | }],
15 | addCube: (x, y, z) => {
16 | set(state => ({
17 | cubes: [...state.cubes, {
18 | id: nanoid(),
19 | texture: state.texture,
20 | pos: [x, y, z]
21 | }]
22 | }))
23 | },
24 | removeCube: (id) => {
25 | set(state => ({
26 | cubes: state.cubes.filter(cube => cube.id !== id)
27 | }))
28 | },
29 | setTexture: (texture) => {
30 | set(() => ({ texture }))
31 | },
32 | saveWorld: () => {},
33 | resetWorld: () => {}
34 | }))
35 |
--------------------------------------------------------------------------------
/src/images/dirt.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/minecraft-clone/36a1323dea6be83e75eed5795d379fd5c6591031/src/images/dirt.jpg
--------------------------------------------------------------------------------
/src/images/glass.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/minecraft-clone/36a1323dea6be83e75eed5795d379fd5c6591031/src/images/glass.png
--------------------------------------------------------------------------------
/src/images/grass.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/minecraft-clone/36a1323dea6be83e75eed5795d379fd5c6591031/src/images/grass.jpg
--------------------------------------------------------------------------------
/src/images/images.js:
--------------------------------------------------------------------------------
1 | export { default as grassImg } from './grass.jpg'
2 | export { default as glassImg } from './glass.png'
3 | export { default as dirtImg } from './dirt.jpg'
4 | export { default as logImg } from './log.jpg'
5 | export { default as woodImg } from './wood.png'
6 |
--------------------------------------------------------------------------------
/src/images/log.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/minecraft-clone/36a1323dea6be83e75eed5795d379fd5c6591031/src/images/log.jpg
--------------------------------------------------------------------------------
/src/images/textures.js:
--------------------------------------------------------------------------------
1 | import {
2 | grassImg,
3 | dirtImg,
4 | logImg,
5 | glassImg,
6 | woodImg
7 | } from './images.js'
8 |
9 | import { NearestFilter, RepeatWrapping, TextureLoader } from 'three'
10 |
11 | const grassTexture = new TextureLoader().load(grassImg)
12 | const dirtTexture = new TextureLoader().load(dirtImg)
13 | const logTexture = new TextureLoader().load(logImg)
14 | const glassTexture = new TextureLoader().load(glassImg)
15 | const woodTexture = new TextureLoader().load(woodImg)
16 |
17 | const groundTexture = new TextureLoader().load(grassImg)
18 |
19 | groundTexture.wrapS = RepeatWrapping
20 | groundTexture.wrapT = RepeatWrapping
21 |
22 | groundTexture.magFilter = NearestFilter
23 | grassTexture.magFilter = NearestFilter
24 | dirtTexture.magFilter = NearestFilter
25 | logTexture.magFilter = NearestFilter
26 | glassTexture.magFilter = NearestFilter
27 | woodTexture.magFilter = NearestFilter
28 |
29 | export {
30 | groundTexture,
31 | grassTexture,
32 | dirtTexture,
33 | logTexture,
34 | glassTexture,
35 | woodTexture
36 | }
37 |
--------------------------------------------------------------------------------
/src/images/wood.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/minecraft-clone/36a1323dea6be83e75eed5795d379fd5c6591031/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 | user-select: none;
13 | overflow: hidden;
14 | }
15 |
16 | body {
17 | position: fixed;
18 | height: 100vh;
19 | width: 100vw;
20 | overflow: hidden;
21 | top: 0;
22 | bottom: 0;
23 | left: 0;
24 | right: 0;
25 | }
26 |
27 | .pointer {
28 | color: white;
29 | font-size: 40px;
30 | position: absolute;
31 | top: 50%;
32 | left: 50%;
33 | transform: translate(-50%, -50%);
34 | opacity: .5;
35 | z-index: 0;
36 | }
37 |
38 | .texture-selector {
39 | bottom: 32px;
40 | background: #aaa;
41 | padding: 8px;
42 | border: 3px solid #000;
43 | position: absolute;
44 | left: 50%;
45 | transform: translateX(-50%);
46 | display: flex;
47 | gap: 8px;
48 | }
49 |
50 | .texture-selector.hidden {
51 | display: none;
52 | }
53 |
54 | .texture-selector img {
55 | width: 50px;
56 | z-index: 100;
57 | image-rendering: pixelated;
58 | border: 3px solid #000;
59 | }
60 |
61 | .texture-selector img.selected {
62 | border-color: red;
63 | }
64 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import App from './App'
4 | import './index.css'
5 |
6 | createRoot(document.getElementById('root')).render(
7 |
8 |
9 |
10 | )
11 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()]
7 | })
8 |
--------------------------------------------------------------------------------