Hello, I have a small problem and I can’t seem to find any solution.
I made a small interactive game where a pawn is moved in a maze by selecting cards given randomly to the user.
I have multiple models imported in my scene, they are all .glb files.
At the start, I display a progress bar indicating the percentage of the total loading, when the loading is at 100%, the scene is shown.
However, whenever I click and choose the first card to play (It only does this on the first chosen card, after this, it never pops up again), the loading screen comes back quickly and then goes away again.
I tried to find what was the problem and I can’t understand what is causing this. I am still inexperienced with three js and less experienced with react three fiber.
Here are the codes of my 3 components for the cards management :
"use client";
import { useRouter } from "next/navigation";
import React, { Suspense, useRef, useState } from "react";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import * as THREE from "three";
import { Mutex } from "async-mutex";
// Used to check if the element are placed correctly
import { Html, OrbitControls, useProgress } from "@react-three/drei";
// Custom types
import { CardType } from "@/config/cardconfig";
import { BoardAnimationHandle, Cell, Maze } from "@/types/board";
// Custom component - 3D Meshes
import Board from "@/components/game/3d/Board";
import Hand from "@/components/game/3d/Hand";
import { Progress } from "@/components/ui/progress";
function LoadingIndicator() {
const { progress } = useProgress();
return (
<Html
className="flex flex-col items-center justify-center px-40 w-[100vw] h-[100vh] bg-secondary gap-y-5"
center
>
<div className="font-portfolioMedieval text-2xl">Loading</div>
<div className="font-portfolioSubtitle text-lg">
{progress.toFixed(2)} %
</div>
<Progress value={progress} />
</Html>
);
}
// This component is used as the Game Controller of the app => all the game logic will depart from here
const Scene = () => {
// Router
const router = useRouter();
// Mutex
const [mutex, setMutex] = useState(new Mutex());
// Get the viewport
const { viewport } = useThree();
// Scale function for the board and the hand of cards
const boardScale = viewport.width * 0.0801 + 0.2214;
const handScale = viewport.width * 0.0162381597 + 0.8421650877;
// const handScale = 0.88;
// Maze configs
// Generate the maze of the board
const generateMaze = (
rows: number,
cols: number,
treasures: number,
ennemies: number
): Maze => {
// Generate array for the maze full of walls
const maze: Maze = {
paths: Array.from({ length: rows }, (_, y) =>
Array.from({ length: cols }, (_, x) => ({
x,
y,
top: true,
right: true,
bottom: true,
left: true,
visited: false,
}))
),
};
// Create an array of cells to be visited (to form the maze)
const stack: Cell[] = [];
let currentCell: Cell = maze.paths[0][0];
currentCell.visited = true;
stack.push(currentCell);
// Visit "all" cells of the maze, but with creating the walls between them
while (stack.length > 0) {
currentCell = stack.pop()!;
const { x, y } = currentCell;
// Find the neighbors of the current cell
const neighbors = [
{ cell: maze.paths[y - 1]?.[x], direction: "top" },
{ cell: maze.paths[y]?.[x + 1], direction: "right" },
{ cell: maze.paths[y + 1]?.[x], direction: "bottom" },
{ cell: maze.paths[y]?.[x - 1], direction: "left" },
];
neighbors.sort(() => Math.random() - 0.5);
// If the neighbors are alreay "visited", put on a wall
for (const { cell, direction } of neighbors) {
if (cell && !cell.visited) {
// Wall creation within the maze
switch (direction) {
case "top": {
currentCell.top = false;
cell.bottom = false;
}
case "right": {
currentCell.right = false;
cell.left = false;
}
case "bottom": {
currentCell.bottom = false;
cell.top = false;
}
case "left": {
currentCell.left = false;
cell.right = false;
}
}
cell.visited = true;
stack.push(cell);
}
}
}
// Wall creation on the sides of the maze
for (const row of maze.paths) {
for (const cell of row) {
if (cell.x === 0) {
cell.left = true;
}
if (cell.y === 0) {
cell.top = true;
}
if (cell.x === maze.paths[cell.y].length - 1) {
cell.right = true;
}
if (cell.y === maze.paths.length - 1) {
cell.bottom = true;
}
}
}
// Player creation
const playerX = maze.paths[0].length - 1;
const playerY = maze.paths.length - 1;
maze.paths[playerX][playerY].player = true;
//Exit creation
if (
!maze.paths[0][0].exit &&
!maze.paths[0][0].treasure &&
!maze.paths[0][0].ennemy &&
!maze.paths[0][0].player
) {
maze.paths[0][0].exit = true;
}
// Treasure creation
for (let i = 0; i < treasures; i++) {
const treasureX = Math.floor(Math.random() * cols);
const treasureY = Math.floor(Math.random() * rows);
if (
!maze.paths[treasureX][treasureY].exit &&
!maze.paths[treasureX][treasureY].treasure &&
!maze.paths[treasureX][treasureY].ennemy &&
!maze.paths[treasureX][treasureY].player
) {
maze.paths[treasureX][treasureY].treasure = true;
}
}
// Ennemy creation
for (let i = 0; i < ennemies; i++) {
const ennemyX = Math.floor(Math.random() * cols);
const ennemyY = Math.floor(Math.random() * rows);
if (
!maze.paths[ennemyX][ennemyY].exit &&
!maze.paths[ennemyX][ennemyY].treasure &&
!maze.paths[ennemyX][ennemyY].ennemy &&
!maze.paths[ennemyX][ennemyY].player
) {
maze.paths[ennemyX][ennemyY].ennemy = true;
}
}
return maze;
};
// Find the player in the maze
const findPlayer = (): number[] => {
let playerPos = [-1, -1];
maze.current.paths.map((row) => {
row.map((cell) => {
if (cell.player) {
playerPos = [cell.y, cell.x];
}
});
});
return playerPos;
};
// Refs
// Board ref
const boardRef = useRef<BoardAnimationHandle>(null);
// Maze
const maze = useRef(generateMaze(5, 5, 3, 3));
// Animation
// Movement of the board on the mouse movements
useFrame((state, delta) => {
boardRef.current!.boardMeshRef().position.z = THREE.MathUtils.lerp(
boardRef.current!.boardMeshRef().position.z,
(state.pointer.y * Math.PI) / 100,
0.05
);
boardRef.current!.boardMeshRef().position.x = THREE.MathUtils.lerp(
boardRef.current!.boardMeshRef().position.x,
(state.pointer.x * Math.PI) / 100,
0.05
);
});
// Game functions
// Play a card
const onCardUsed = (actionType: CardType, index: number) => {
// Position of the player (y is 1st and x is 2nd)
const playerPosition = findPlayer();
// Dice thrown to get a card number
const diceThrow = Math.floor(Math.random() * 4 + 1);
// The real possible steps
let steps = 0;
// The positions of the encountered treasures
let treasureFlags = [];
// If the pawn encountered the exit
let exitFlag = false;
switch (actionType) {
case CardType.Forward: {
for (let i = 0; i < diceThrow; i++) {
// Exit of the maze
if (playerPosition[0] - i - 1 == 0 && playerPosition[1] == 0) {
exitFlag = true;
}
// Border of the maze
if (playerPosition[0] - i <= 0) {
break;
}
// Wall
if (
maze.current.paths[playerPosition[0] - i][playerPosition[1]].top ||
maze.current.paths[playerPosition[0] - i - 1][playerPosition[1]]
.bottom
) {
break;
}
// Ennemy
if (
maze.current.paths[playerPosition[0] - i - 1][playerPosition[1]]
.ennemy
) {
break;
}
// Treasure
if (playerPosition[0] - i - 1 >= 0) {
if (
maze.current.paths[playerPosition[0] - i - 1][playerPosition[1]]
.treasure
) {
treasureFlags.push([
playerPosition[0] - i - 1,
playerPosition[1],
]);
steps++;
break;
}
}
// Otherwise, count possible steps
steps++;
}
boardRef.current?.moveForward(
steps,
playerPosition,
treasureFlags,
exitFlag
);
break;
}
case CardType.Backward: {
for (let i = 0; i < diceThrow; i++) {
// Exit of the maze
if (playerPosition[0] + i + 1 == 0 && playerPosition[1] == 0) {
exitFlag = true;
}
// Border of the maze
if (playerPosition[0] + i >= maze.current.paths.length - 1) {
break;
}
// Wall
if (
maze.current.paths[playerPosition[0] + i][playerPosition[1]]
.bottom ||
maze.current.paths[playerPosition[0] + i + 1][playerPosition[1]].top
) {
break;
}
// Ennemy
if (
maze.current.paths[playerPosition[0] + i + 1][playerPosition[1]]
.ennemy
) {
break;
}
// Treasure
if (playerPosition[0] + i + 1 < maze.current.paths.length) {
if (
maze.current.paths[playerPosition[0] + i + 1][playerPosition[1]]
.treasure
) {
treasureFlags.push([
playerPosition[0] + i + 1,
playerPosition[1],
]);
steps++;
break;
}
}
// Otherwise, count possible steps
steps++;
}
boardRef.current?.moveBackward(
steps,
playerPosition,
treasureFlags,
exitFlag
);
break;
}
case CardType.Left: {
for (let i = 0; i < diceThrow; i++) {
// Exit of the maze
if (playerPosition[0] == 0 && playerPosition[1] - i - 1 == 0) {
exitFlag = true;
}
// Border of the maze
if (playerPosition[1] - i <= 0) {
break;
}
// Wall
if (
maze.current.paths[playerPosition[0]][playerPosition[1] - i].left ||
maze.current.paths[playerPosition[0]][playerPosition[1] - i - 1]
.right
) {
break;
}
// Ennemy
if (
maze.current.paths[playerPosition[0]][playerPosition[1] - i - 1]
.ennemy
) {
break;
}
// Treasure
if (playerPosition[1] - i - 1 >= 0) {
if (
maze.current.paths[playerPosition[0]][playerPosition[1] - i - 1]
.treasure
) {
treasureFlags.push([
playerPosition[0],
playerPosition[1] - i - 1,
]);
steps++;
break;
}
}
// Otherwise, count possible steps
steps++;
}
boardRef.current?.moveLeft(
steps,
playerPosition,
treasureFlags,
exitFlag
);
break;
}
case CardType.Right: {
for (let i = 0; i < diceThrow; i++) {
// Exit of the maze
if (playerPosition[0] == 0 && playerPosition[1] + i + 1 == 0) {
exitFlag = true;
}
// Border of the maze
if (playerPosition[1] + i >= maze.current.paths[0].length - 1) {
break;
}
// Wall
if (
maze.current.paths[playerPosition[0]][playerPosition[1] + i]
.right ||
maze.current.paths[playerPosition[0]][playerPosition[1] + i + 1]
.left
) {
break;
}
// Ennemy
if (
maze.current.paths[playerPosition[0]][playerPosition[1] + i + 1]
.ennemy
) {
break;
}
// Treasure
if (playerPosition[1] + i + 1 < maze.current.paths[0].length) {
if (
maze.current.paths[playerPosition[0]][playerPosition[1] + i + 1]
.treasure
) {
treasureFlags.push([
playerPosition[0],
playerPosition[1] + i + 1,
]);
steps++;
break;
}
}
// Otherwise, count possible steps
steps++;
}
boardRef.current?.moveRight(
steps,
playerPosition,
treasureFlags,
exitFlag
);
break;
}
case CardType.Attack: {
// Side value
let side = -1;
// Check if there is an enemy around the player - clockwise, an enemy at a time
if (
playerPosition[0] - 1 >= 0 &&
!maze.current.paths[playerPosition[0]][playerPosition[1]].top &&
!maze.current.paths[playerPosition[0] - 1][playerPosition[1]]
.bottom &&
maze.current.paths[playerPosition[0] - 1][playerPosition[1]].ennemy
) {
// Top
side = 0;
}
if (
playerPosition[1] + 1 < maze.current.paths[0].length &&
!maze.current.paths[playerPosition[0]][playerPosition[1]].right &&
!maze.current.paths[playerPosition[0]][playerPosition[1] + 1].left &&
maze.current.paths[playerPosition[0]][playerPosition[1] + 1].ennemy
) {
// Right
side = 1;
}
if (
playerPosition[0] + 1 < maze.current.paths.length &&
!maze.current.paths[playerPosition[0]][playerPosition[1]].bottom &&
!maze.current.paths[playerPosition[0] + 1][playerPosition[1]].top &&
maze.current.paths[playerPosition[0] + 1][playerPosition[1]].ennemy
) {
// Bottom
side = 2;
}
if (
playerPosition[1] - 1 >= 0 &&
!maze.current.paths[playerPosition[0]][playerPosition[1]].left &&
!maze.current.paths[playerPosition[0]][playerPosition[1] - 1].right &&
maze.current.paths[playerPosition[0]][playerPosition[1] - 1].ennemy
) {
// Left
side = 3;
}
// Attack the nearest enemy
boardRef.current?.attack(side, playerPosition);
break;
}
}
};
// Change the position of the player in the maze and update the maze
const playerMovement = (
mazeB: Maze,
newPos: number[],
treasure: boolean
): void => {
mazeB.paths.map((row) => {
row.map((cell) => {
if (cell.player) {
cell.player = false;
}
});
});
// y is the row, y is the col
mazeB.paths[newPos[0]][newPos[1]].player = true;
if (treasure) {
mazeB.paths[newPos[0]][newPos[1]].treasure = false;
}
maze.current = mazeB;
};
// Delete the attacked ene
const playerAttack = (mazeB: Maze, enemyPos: number[]): void => {
mazeB.paths[enemyPos[0]][enemyPos[1]].ennemy = false;
maze.current = mazeB;
};
const playerExit = () => {
router.push("/portfolio#myself");
};
return (
<>
<Board
ref={boardRef}
mutex={mutex}
maze={maze.current}
scale={boardScale}
playerMovement={playerMovement}
playerAttack={playerAttack}
playerExit={playerExit}
/>
<Hand mutex={mutex} scale={handScale} onCardUsed={onCardUsed} />
</>
);
};
const Page = () => {
return (
<div className="h-screen">
{/* Camera options */}
<Canvas
camera={{ fov: 45, near: 0.1, far: 1000, position: [0, 4.8, 3.3] }}
shadows={true}
>
<Suspense fallback={<LoadingIndicator />}>
{/* Lights */}
<ambientLight intensity={0.3} color={0xa3a3a3}></ambientLight>
<directionalLight
intensity={0.8}
color={0xffffff}
position={[0, 10, 0]}
castShadow
shadow-mapSize={[1024, 1024]}
/>
<pointLight
position={[-4.5, 2, 0]}
intensity={9}
color={"#c56f28"}
></pointLight>
<pointLight
position={[4.5, 2, 0]}
intensity={9}
color={"#c56f28"}
></pointLight>
<pointLight
position={[0, 2, -3]}
intensity={9}
color={"#c56f28"}
></pointLight>
<pointLight
position={[0, 2, 4]}
intensity={9}
color={"#c56f28"}
></pointLight>
<directionalLight
intensity={1.5}
color={"#c56f28"}
position={[0, 10, 10]}
castShadow
shadow-mapSize={[1024, 1024]}
/>
{/* Scene and the game */}
<Scene />
</Suspense>
</Canvas>
</div>
);
};
export default Page;
import React, { useRef, useState } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { Mesh } from "three";
import Card from "./Card";
import { CardInfo, HandProp } from "@/types/hand";
import { CardsPositions, UsePos, UseRot } from "@/config/handconst";
import { CARD_CONFIG } from "@/config/cardconfig";
import { ThreeEvent } from "@react-three/fiber";
import { Html } from "@react-three/drei";
import { Button, buttonVariants } from "@/components/ui/button";
import { Shuffle } from "lucide-react";
import { cn } from "@/lib/utils";
const Hand = ({ mutex, scale, onCardUsed }: HandProp) => {
// GSAP
const { contextSafe } = useGSAP();
// Generating a number between 1 and 5 (for the card configuration) (1 and 5 included)
const generateRandomCardConfig = () => {
return Math.floor(Math.random() * 5);
};
const generateRandomHand = (): CardInfo[] => {
const initialHand: CardInfo[] = [];
const indices = new Set<number>();
while (indices.size < 4) {
indices.add(generateRandomCardConfig());
}
indices.forEach((index) => {
initialHand.push({ cardConfig: CARD_CONFIG[index] });
});
return initialHand;
};
// How many shuffle is possible
const [shuffle, setShuffle] = useState<number>(5);
// Hand of cards and their refs
const [hand, setHand] = useState(generateRandomHand());
// Refs on the cards
const cardsRefs = useRef<Mesh[]>([]);
const buttonRef = useRef<HTMLDivElement>(null);
// How many card left
const cardQuantityIndex = hand.length - 1;
// If a card has been clicked
const isClicked = useRef<boolean>(false);
// Shuffle a new hand
const shuffleHand = () => {
cardsRefs.current = [];
setHand(generateRandomHand());
};
// Animation when the card is hovered in by the mouse
const HoverIn = contextSafe((index: number, e: ThreeEvent<PointerEvent>) => {
e.stopPropagation();
if (!isClicked.current) {
// Move the hovered card
gsap.to(cardsRefs.current[index]?.position!, {
x: CardsPositions[cardQuantityIndex].HoverPos[index][0],
y: CardsPositions[cardQuantityIndex].HoverPos[index][1],
z: CardsPositions[cardQuantityIndex].HoverPos[index][2],
});
// Rotate the hovered card
gsap.to(cardsRefs.current[index]?.rotation!, {
x: CardsPositions[cardQuantityIndex].HoverRot[index][0],
y: CardsPositions[cardQuantityIndex].HoverRot[index][1],
z: CardsPositions[cardQuantityIndex].HoverRot[index][2],
});
// Move the other cards to move them apart from the hovered one
for (let i = 0; i <= cardQuantityIndex; i++) {
// If the card exist and is not the hovered one, move it to the correct position
if (cardsRefs.current[i] && i != index) {
gsap.to(cardsRefs.current[i]?.position!, {
x: CardsPositions[cardQuantityIndex].MoveHoverPos[i][0],
y: CardsPositions[cardQuantityIndex].MoveHoverPos[i][1],
z: CardsPositions[cardQuantityIndex].MoveHoverPos[i][2],
});
}
}
}
});
// Animation when the card is hovered out by the mouse
const HoverOut = contextSafe((index: number, e: ThreeEvent<PointerEvent>) => {
e.stopPropagation();
if (!isClicked.current) {
// Move the hovered card to its base position
gsap.to(cardsRefs.current[index]?.position!, {
x: CardsPositions[cardQuantityIndex].BasePos[index][0],
y: CardsPositions[cardQuantityIndex].BasePos[index][1],
z: CardsPositions[cardQuantityIndex].BasePos[index][2],
});
// Rotate the hovered card to it base rotation
gsap.to(cardsRefs.current[index]?.rotation!, {
x: CardsPositions[cardQuantityIndex].BaseRot[index][0],
y: CardsPositions[cardQuantityIndex].BaseRot[index][1],
z: CardsPositions[cardQuantityIndex].BaseRot[index][2],
});
// Move the other cards to move them apart from the hovered one
for (let i = 0; i <= cardQuantityIndex; i++) {
// If the card exist and is not the hovered one, move it to the correct position
if (cardsRefs.current[i] && i != index) {
gsap.to(cardsRefs.current[i]?.position!, {
x: CardsPositions[cardQuantityIndex].BasePos[i][0],
y: CardsPositions[cardQuantityIndex].BasePos[i][1],
z: CardsPositions[cardQuantityIndex].BasePos[i][2],
});
}
}
}
});
// Animation and use of the card when the card is clicked
const ClickOn = contextSafe((index: number, e: ThreeEvent<MouseEvent>) => {
e.stopPropagation();
mutex.acquire().then(() => {
if (!isClicked.current) {
isClicked.current = true;
let nextPos = cardQuantityIndex - 1;
gsap.to(cardsRefs.current[index]?.position!, {
x: UsePos[0],
y: UsePos[1],
z: UsePos[2],
});
// Move the other cards to their next base position
let count = 0;
for (let i = 0; i <= cardQuantityIndex; i++) {
if (cardsRefs.current[i] && i != index) {
gsap.to(cardsRefs.current[i]?.position!, {
x: CardsPositions[nextPos].BasePos[count][0],
y: CardsPositions[nextPos].BasePos[count][1],
z: CardsPositions[nextPos].BasePos[count][2],
});
gsap.to(cardsRefs.current[i]?.rotation!, {
x: CardsPositions[nextPos].BaseRot[count][0],
y: CardsPositions[nextPos].BaseRot[count][1],
z: CardsPositions[nextPos].BaseRot[count][2],
});
count++;
}
}
gsap.to(cardsRefs.current[index]?.rotation!, {
x: UseRot[0],
y: UseRot[1],
z: UseRot[2],
onComplete: () => {
// Play the card
onCardUsed(hand[index].cardConfig.cardType, index);
// Update the cards references
cardsRefs.current = cardsRefs.current?.filter(
(_, i) => i !== index
);
// Use the card and update the hand
setHand(hand.filter((_, i) => i !== index));
// Allow the possibility to click again
isClicked.current = false;
// When all the cards are used, shuffle new cards
if (hand.length <= 1) {
shuffleHand();
}
},
});
}
});
});
// Click on the button to do a new shuffle
const manualShuffle = () => {
shuffleHand();
setShuffle(shuffle - 1);
};
return (
<mesh scale={scale}>
{shuffle > 0 && (
<Html className="w-[100vw] h-[100vh] fixed inset-0 flex justify-center items-center pointer-events-none">
<div ref={buttonRef} className="absolute -top-[48.5%] -left-[48.5%]">
<Button
className={cn(
"gap-x-3 pointer-events-auto",
buttonVariants({
variant: "secondary",
className:
"w-24 h-11 font-portfolioMedieval text-primary text-3xl",
})
)}
onClick={manualShuffle}
>
{shuffle}
<Shuffle className="h-8 w-8" />
</Button>
</div>
</Html>
)}
{hand.map((card, index) => (
<Card
ref={(cardElem) => (cardsRefs.current[index] = cardElem!)}
key={index}
index={index}
cardConfig={card.cardConfig}
position={CardsPositions[cardQuantityIndex].BasePos[index]}
rotation={CardsPositions[cardQuantityIndex].BaseRot[index]}
hoverIn={HoverIn}
hoverOut={HoverOut}
clickOn={ClickOn}
/>
))}
</mesh>
);
};
export default Hand;
import React from "react";
import { Mesh } from "three";
import { ThreeEvent, useLoader } from "@react-three/fiber";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { CardProp } from "@/types/hand";
import { useGLTF } from "@react-three/drei";
// A card to be used in the game
const Card = React.forwardRef<Mesh, CardProp>((props, ref) => {
const gltf = useLoader(GLTFLoader, props.cardConfig.frontTexture);
return (
<group>
<primitive
ref={ref}
object={gltf.scene.clone()}
position={props.position}
rotation={props.rotation}
scale={0.0315}
onPointerEnter={(e: ThreeEvent<PointerEvent>) =>
props.hoverIn(props.index, e)
}
onPointerLeave={(e: ThreeEvent<PointerEvent>) =>
props.hoverOut(props.index, e)
}
onClick={(e: ThreeEvent<MouseEvent>) => props.clickOn(props.index, e)}
></primitive>
</group>
);
});
Card.displayName = "Card";
export default Card;
Thank you in advance, I stuck right now