mirror of
https://github.com/Floriansylvain/generic-threejs-game.git
synced 2026-08-19 11:43:18 +02:00
feat: better optimized chunk generation
This commit is contained in:
+2
-16
@@ -2,16 +2,13 @@ import "./App.css";
|
|||||||
|
|
||||||
import { Canvas } from "@react-three/fiber";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import { KeyboardControls } from "@react-three/drei";
|
import { KeyboardControls } from "@react-three/drei";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { PCFSoftShadowMap, Vector3 } from "three";
|
import { PCFSoftShadowMap, Vector3 } from "three";
|
||||||
import { Player } from "./components/Player";
|
import { Player } from "./components/Player";
|
||||||
import { Bloom, EffectComposer, Vignette } from "@react-three/postprocessing";
|
import { Bloom, EffectComposer, Vignette } from "@react-three/postprocessing";
|
||||||
import { Cubes } from "./components/Cubes";
|
|
||||||
import { GameContext } from "./hooks/GameContext";
|
import { GameContext } from "./hooks/GameContext";
|
||||||
import { ChunkGenerator } from "./components/ChunkGenerator";
|
import { ChunkGenerator } from "./components/ChunkGenerator";
|
||||||
|
|
||||||
const CUBES_QT = 10;
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const container = useRef<HTMLCanvasElement>(null);
|
const container = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
@@ -19,15 +16,6 @@ function App() {
|
|||||||
container.current?.focus();
|
container.current?.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
const [cubes] = useState<Vector3[]>(
|
|
||||||
Array.from({ length: CUBES_QT }, (_, i) =>
|
|
||||||
Array.from(
|
|
||||||
{ length: CUBES_QT },
|
|
||||||
(_, j) => new Vector3(i + 20 - CUBES_QT / 2, 0, j - CUBES_QT / 2)
|
|
||||||
)
|
|
||||||
).flat()
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
className="container"
|
className="container"
|
||||||
@@ -37,10 +25,8 @@ function App() {
|
|||||||
className="canvas"
|
className="canvas"
|
||||||
ref={container}
|
ref={container}
|
||||||
shadows={{ type: PCFSoftShadowMap }}
|
shadows={{ type: PCFSoftShadowMap }}
|
||||||
camera={{ fov: 50, frustumCulled: true, near: 0.1, far: 200 }}
|
camera={{ fov: 50, frustumCulled: true, near: 0.1, far: 2000 }}
|
||||||
>
|
>
|
||||||
<Cubes cubesPosition={cubes}></Cubes>
|
|
||||||
|
|
||||||
<GameContext.Provider value={{ playerPosition: new Vector3() }}>
|
<GameContext.Provider value={{ playerPosition: new Vector3() }}>
|
||||||
<KeyboardControls
|
<KeyboardControls
|
||||||
map={[
|
map={[
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { memo, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
ClampToEdgeWrapping,
|
ClampToEdgeWrapping,
|
||||||
FrontSide,
|
FrontSide,
|
||||||
@@ -15,8 +15,8 @@ const grassTexture = new TextureLoader().load("./texture_grass.jpg");
|
|||||||
grassTexture.wrapS = grassTexture.wrapT = RepeatWrapping;
|
grassTexture.wrapS = grassTexture.wrapT = RepeatWrapping;
|
||||||
grassTexture.repeat.set(100, 100);
|
grassTexture.repeat.set(100, 100);
|
||||||
|
|
||||||
const SCALE = 5;
|
const SCALE = 1;
|
||||||
const AMPLITUDE = 10;
|
const AMPLITUDE = 100;
|
||||||
const GEOMETRY = 50;
|
const GEOMETRY = 50;
|
||||||
|
|
||||||
const SIZE = 100; // CHANGE WITH CAUTION
|
const SIZE = 100; // CHANGE WITH CAUTION
|
||||||
@@ -27,7 +27,8 @@ const ADJUSTED_GEOMETRY = GEOMETRY + OVERLAP * 2;
|
|||||||
const ADJUSTED_SIZE = SIZE + OVERLAP * 2;
|
const ADJUSTED_SIZE = SIZE + OVERLAP * 2;
|
||||||
const ADJUSTED_SCALE = SIZE / SCALE;
|
const ADJUSTED_SCALE = SIZE / SCALE;
|
||||||
|
|
||||||
export function Chunk(props: { position: Vector2; seed: number }): JSX.Element {
|
export const Chunk = memo(
|
||||||
|
(props: { position: Vector2; seed: number }): JSX.Element => {
|
||||||
const noise = (x: number, y: number): number => {
|
const noise = (x: number, y: number): number => {
|
||||||
const noise = new ImprovedNoise();
|
const noise = new ImprovedNoise();
|
||||||
return noise.noise(x, y, props.seed);
|
return noise.noise(x, y, props.seed);
|
||||||
@@ -46,7 +47,10 @@ export function Chunk(props: { position: Vector2; seed: number }): JSX.Element {
|
|||||||
for (let x = 0; x < ADJUSTED_SIZE; x++) {
|
for (let x = 0; x < ADJUSTED_SIZE; x++) {
|
||||||
for (let y = 0; y < ADJUSTED_SIZE; y++) {
|
for (let y = 0; y < ADJUSTED_SIZE; y++) {
|
||||||
const n =
|
const n =
|
||||||
noise((x + offsetX) / ADJUSTED_SCALE, (y + offsetY) / ADJUSTED_SCALE) *
|
noise(
|
||||||
|
(x + offsetX) / ADJUSTED_SCALE,
|
||||||
|
(y + offsetY) / ADJUSTED_SCALE
|
||||||
|
) *
|
||||||
127 +
|
127 +
|
||||||
127;
|
127;
|
||||||
const id = (x + y * ADJUSTED_SIZE) * 4;
|
const id = (x + y * ADJUSTED_SIZE) * 4;
|
||||||
@@ -76,7 +80,12 @@ export function Chunk(props: { position: Vector2; seed: number }): JSX.Element {
|
|||||||
>
|
>
|
||||||
<planeGeometry
|
<planeGeometry
|
||||||
attach="geometry"
|
attach="geometry"
|
||||||
args={[CHUNK_SIZE, CHUNK_SIZE, ADJUSTED_GEOMETRY, ADJUSTED_GEOMETRY]}
|
args={[
|
||||||
|
CHUNK_SIZE,
|
||||||
|
CHUNK_SIZE,
|
||||||
|
ADJUSTED_GEOMETRY,
|
||||||
|
ADJUSTED_GEOMETRY,
|
||||||
|
]}
|
||||||
></planeGeometry>
|
></planeGeometry>
|
||||||
<meshStandardMaterial
|
<meshStandardMaterial
|
||||||
toneMapped={false}
|
toneMapped={false}
|
||||||
@@ -90,3 +99,4 @@ export function Chunk(props: { position: Vector2; seed: number }): JSX.Element {
|
|||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,53 +1,111 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useRef, useState } from "react";
|
||||||
import { GameContext } from "../hooks/GameContext";
|
import { GameContext } from "../hooks/GameContext";
|
||||||
import { useFrame } from "@react-three/fiber";
|
import { useFrame } from "@react-three/fiber";
|
||||||
import { Vector2 } from "three";
|
import { Vector2 } from "three";
|
||||||
import { Chunk } from "./Chunk";
|
import { Chunk } from "./Chunk";
|
||||||
|
|
||||||
|
const CHUNK_RADIUS = 20;
|
||||||
|
const CHUNK_PER_FRAME = 3;
|
||||||
|
|
||||||
export function ChunkGenerator(): JSX.Element {
|
export function ChunkGenerator(): JSX.Element {
|
||||||
const { playerPosition } = useContext(GameContext);
|
const { playerPosition } = useContext(GameContext);
|
||||||
const [lastPlayerPosition] = useState(new Vector2(0, 0));
|
const [lastPlayerPosition] = useState(new Vector2());
|
||||||
|
|
||||||
const [chunks, setChunks] = useState([] as JSX.Element[]);
|
const [chunks, setChunks] = useState([] as JSX.Element[]);
|
||||||
|
const [chunkQueue, setChunkQueue] = useState([] as JSX.Element[]);
|
||||||
|
const generatedChunkKeys = useRef(new Set<string>());
|
||||||
|
|
||||||
const chunkPosVec2 = new Vector2();
|
const chunkPosVec2 = new Vector2();
|
||||||
|
|
||||||
const isChunkAlreadyGenerated = (x: number, y: number) =>
|
const getChunkCoordinates = (x: number, y: number): number[] => {
|
||||||
chunks.some((chunk) => chunk.key === `${x}-${y}`);
|
return [Math.floor(x / 100 + 0.5), Math.floor(y / 100 + 0.5)];
|
||||||
|
};
|
||||||
|
|
||||||
function generateNewChunks(playerX: number, playerY: number) {
|
const isChunkAlreadyGenerated = (x: number, y: number, key: string) =>
|
||||||
const newChunks = [];
|
generatedChunkKeys.current.has(`${x}-${y}`) ||
|
||||||
for (let x = -2; x <= 2; x++) {
|
chunkQueue.some((chunk) => chunk.key === key);
|
||||||
for (let y = -2; y <= 2; y++) {
|
|
||||||
if (isChunkAlreadyGenerated(playerX + x, playerY + y)) continue;
|
const isChunkInPlayerRadius = (chunkPos: number[], playerPos: number[]) =>
|
||||||
|
chunkPos[0] < playerPos[0] - CHUNK_RADIUS ||
|
||||||
|
chunkPos[0] > playerPos[0] + CHUNK_RADIUS ||
|
||||||
|
chunkPos[1] < playerPos[1] - CHUNK_RADIUS ||
|
||||||
|
chunkPos[1] > playerPos[1] + CHUNK_RADIUS;
|
||||||
|
|
||||||
|
function generateNewChunks(playerX: number, playerY: number): void {
|
||||||
|
const newChunks = [] as JSX.Element[];
|
||||||
|
for (let x = -CHUNK_RADIUS; x <= CHUNK_RADIUS; x++) {
|
||||||
|
for (let y = -CHUNK_RADIUS; y <= CHUNK_RADIUS; y++) {
|
||||||
|
const key = `${playerX + x}-${playerY + y}`;
|
||||||
|
if (isChunkAlreadyGenerated(playerX + x, playerY + y, key)) continue;
|
||||||
newChunks.push(
|
newChunks.push(
|
||||||
<Chunk
|
<Chunk
|
||||||
key={`${playerX + x}-${playerY + y}`}
|
key={key}
|
||||||
position={chunkPosVec2
|
position={chunkPosVec2
|
||||||
.clone()
|
.clone()
|
||||||
.set((playerX + x) * 100, (playerY + y) * 100)}
|
.set((playerX + x) * 100, (playerY + y) * 100)}
|
||||||
seed={0.25386}
|
seed={0.25386}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
generatedChunkKeys.current.add(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setChunks([...chunks, ...newChunks]);
|
setChunkQueue((prevQueue) => [...prevQueue, ...newChunks]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilteredChunks(prevChunks: JSX.Element[], playerPos: number[]) {
|
||||||
|
const filteredPrevChunks = [] as JSX.Element[];
|
||||||
|
|
||||||
|
let qtDeleted = 0;
|
||||||
|
prevChunks.forEach((chunk) => {
|
||||||
|
const chunkPos = getChunkCoordinates(
|
||||||
|
chunk.props.position.x,
|
||||||
|
chunk.props.position.y
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
qtDeleted < CHUNK_PER_FRAME &&
|
||||||
|
isChunkInPlayerRadius(chunkPos, playerPos)
|
||||||
|
) {
|
||||||
|
generatedChunkKeys.current.delete(`${chunkPos[0]}-${chunkPos[1]}`);
|
||||||
|
qtDeleted++;
|
||||||
|
} else {
|
||||||
|
filteredPrevChunks.push(chunk);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return filteredPrevChunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNewChunks(queuedChunks: JSX.Element[], playerPos: number[]) {
|
||||||
|
setChunks((prevChunks) => {
|
||||||
|
const newChunks = queuedChunks
|
||||||
|
.splice(-CHUNK_PER_FRAME)
|
||||||
|
.filter(
|
||||||
|
(chunk) =>
|
||||||
|
!prevChunks.some((prevChunk) => prevChunk.key === chunk.key)
|
||||||
|
);
|
||||||
|
const filteredPrevChunks = getFilteredChunks(prevChunks, playerPos);
|
||||||
|
return [...filteredPrevChunks, ...newChunks];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
const playerX = Math.floor(playerPosition.x / 100 + 0.5);
|
const playerPos = getChunkCoordinates(playerPosition.x, playerPosition.z);
|
||||||
const playerY = Math.floor(playerPosition.z / 100 + 0.5);
|
|
||||||
|
|
||||||
if (lastPlayerPosition.x === playerX && lastPlayerPosition.y === playerY)
|
if (chunkQueue.length > 0) {
|
||||||
return;
|
setChunkQueue((prevQueue) => {
|
||||||
|
setNewChunks(prevQueue, playerPos);
|
||||||
generateNewChunks(playerX, playerY);
|
return prevQueue;
|
||||||
|
|
||||||
lastPlayerPosition.set(playerX, playerY);
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
if (
|
||||||
generateNewChunks(0, 0);
|
lastPlayerPosition.x !== playerPos[0] ||
|
||||||
}, []);
|
lastPlayerPosition.y !== playerPos[1]
|
||||||
|
) {
|
||||||
|
generateNewChunks(playerPos[0], playerPos[1]);
|
||||||
|
lastPlayerPosition.set(playerPos[0], playerPos[1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return <>{chunks}</>;
|
return <>{chunks}</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ export interface KeyPressed {
|
|||||||
sprint: boolean;
|
sprint: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PLAYER_SPEED = 4.75 * 10;
|
const PLAYER_SPEED = 4.5 * 100;
|
||||||
const DRAW_DISTANCE = 128;
|
|
||||||
const SHADOW_RESOLUTION = 2048;
|
const SHADOW_RESOLUTION = 2048;
|
||||||
const MOUSE_X_SENSITIVITY = 0.002;
|
const MOUSE_X_SENSITIVITY = 0.002;
|
||||||
const MOUSE_Y_SENSITIVITY = 0.001;
|
const MOUSE_Y_SENSITIVITY = 0.001;
|
||||||
@@ -30,7 +29,7 @@ const ANIM_SPEED = 0.2;
|
|||||||
const CAMERA_RADIUS = 2;
|
const CAMERA_RADIUS = 2;
|
||||||
const CAMERA_HEIGHT = 1.5;
|
const CAMERA_HEIGHT = 1.5;
|
||||||
|
|
||||||
const SHADOW_DRAW_DISTANCE = DRAW_DISTANCE * 1.2;
|
const SHADOW_DRAW_DISTANCE = 128;
|
||||||
|
|
||||||
export function Player(): JSX.Element {
|
export function Player(): JSX.Element {
|
||||||
const model = useGLTF("/Adventurer.glb");
|
const model = useGLTF("/Adventurer.glb");
|
||||||
@@ -181,7 +180,7 @@ export function Player(): JSX.Element {
|
|||||||
function setDirLightPosition(): void {
|
function setDirLightPosition(): void {
|
||||||
dirLightRef.current?.position.set(
|
dirLightRef.current?.position.set(
|
||||||
model.scene.position.x + 100,
|
model.scene.position.x + 100,
|
||||||
70,
|
model.scene.position.y + 100,
|
||||||
model.scene.position.z + 100
|
model.scene.position.z + 100
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -203,7 +202,7 @@ export function Player(): JSX.Element {
|
|||||||
child.receiveShadow = true;
|
child.receiveShadow = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
model.scene.position.y = 10;
|
model.scene.position.y = 70;
|
||||||
if (dirLightRef.current) {
|
if (dirLightRef.current) {
|
||||||
dirLightRef.current.shadow.bias = -0.0001;
|
dirLightRef.current.shadow.bias = -0.0001;
|
||||||
}
|
}
|
||||||
@@ -234,10 +233,7 @@ export function Player(): JSX.Element {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</directionalLight>
|
</directionalLight>
|
||||||
<fog
|
<fog attach={"fog"} args={["lightblue", 1000, 2000]}></fog>
|
||||||
attach={"fog"}
|
|
||||||
args={["lightblue", DRAW_DISTANCE * 0.8, DRAW_DISTANCE]}
|
|
||||||
></fog>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user