230 lines
8 KiB
TeX
230 lines
8 KiB
TeX
\documentclass{article}
|
|
|
|
|
|
\usepackage[T1]{fontenc} % encodage
|
|
\renewcommand{\familydefault}{\sfdefault} % police en sans-serif
|
|
|
|
\usepackage[french]{babel} % langue
|
|
\frenchsetup{SmallCapsFigTabCaptions=false}
|
|
|
|
\usepackage[hidelinks]{hyperref} % liens cliquable dans la table des matières
|
|
|
|
\usepackage{graphicx} % images
|
|
\usepackage{caption}
|
|
|
|
\usepackage[a4paper, left=20mm, top=20mm]{geometry} % dimensions de la page
|
|
|
|
\usepackage{minted} % intégration code
|
|
\usemintedstyle{emacs}
|
|
|
|
\title{Projet - GeometryDash 3D}
|
|
\author{\href{mailto:anri.kennel@etud.univ-paris8.fr}{Anri Kennel}
|
|
\thanks{Numéro d'étudiant : 20010664}\, (L3-A)
|
|
\\Moteurs de jeu $\cdot$ Université Paris 8}
|
|
\date{Année universitaire 2022-2023}
|
|
|
|
\begin{document}
|
|
\maketitle
|
|
\tableofcontents
|
|
\clearpage
|
|
|
|
\section[Projet]{Explication du projet}
|
|
Le but du projet est de créer un jeu du type
|
|
\href{https://fr.wikipedia.org/wiki/Geometry_Dash}{Geometry Dash} en vue 3D.
|
|
Geometry Dash est un jeu de plateforme dans lequel le joueur dirige un cube
|
|
glissant sur le sol et qui doit sauter pour éviter des obstacles.
|
|
|
|
Le but de \textbf{ce} jeu est de faire le plus gros score, chaque seconde
|
|
passée dans le jeu ajoute 1 point au compteur, le score étant affiché en haut de l'écran.
|
|
|
|
\section[Rendu 3D]{Rendu 3D avec THREE.JS}
|
|
La \autoref{fig:expls} schématise grossièrement comment est rendu le jeu.
|
|
|
|
\begin{figure}[h]
|
|
\centering
|
|
\includegraphics[width=0.8\textwidth]{presentation/imgs/explications.png}
|
|
\caption{Explications du rendu d'une scène}
|
|
\label{fig:expls}
|
|
\end{figure}
|
|
|
|
L'environnement du jeu s'occupe de tout. Elle garde en mémoire la liste des
|
|
éléments présents dans le jeu :
|
|
\begin{itemize}
|
|
\item Joueur (le cube)
|
|
\item Ennemis (les piques)
|
|
\end{itemize}
|
|
|
|
Elle lance l'animation de ces derniers chacun son tour dans la fonction
|
|
\texttt{animate}, qui est appelée juste avant de rendre la scène (cf. \autoref{cod:envupdate}).
|
|
|
|
\begin{figure}[h]
|
|
\centering
|
|
\begin{minipage}{0.3\textwidth}
|
|
\begin{minted}[autogobble,linenos]{js}
|
|
update = (demo) => {
|
|
if (this.animate(demo)) {
|
|
return true;
|
|
}
|
|
this.render();
|
|
|
|
return false;
|
|
};
|
|
\end{minted}
|
|
\end{minipage}
|
|
\caption{fonction \texttt{update}, \texttt{Env.js}}
|
|
\label{cod:envupdate}
|
|
\end{figure}
|
|
|
|
|
|
\section[Logique]{Logique du jeu}
|
|
Lorsque l'on joue, on a l'impression que le personnage avance sur un plan
|
|
infini. Ce n'est pas exactement ce qui se passe.
|
|
|
|
Le joueur est un cube fixe, il ne fait que sauté et tourné sur lui-même.
|
|
Le plan est fixe, ce qui bouge sont les piques ennemis.
|
|
|
|
\subsection[Génération procédurale]{Génération du terrain}
|
|
Le sol est un plan simple sur lequel une texture en dégradé est appliquée,
|
|
du blanc (proche de la caméra) au noir. Il y a une lumière pour appuyer l'effet
|
|
3D de la scène (il y a donc les ombres des objets projetés sur le sol).
|
|
|
|
Le monde est généré via la fonction \texttt{generateRandomMap} de la classe
|
|
\texttt{Env}. Elle permet de générer $n$ obstacles pour le joueur placés devant
|
|
le joueur. Quand les obstacles passent derrière le joueur et sortent de l'écran,
|
|
ils sont téléportés devant le joueur en étant déplacé, ils ne sont pas justes
|
|
replacés à leur position initiale, créant une carte infinie. La taille du pique est choisie de façon aléatoire.
|
|
|
|
\begin{figure}[h]
|
|
\centering
|
|
\begin{minipage}{0.7\textwidth}
|
|
\begin{minted}[autogobble,linenos]{js}
|
|
const spade = new Spade(
|
|
Math.random() * 0xffffff,
|
|
Math.round(Math.random()) ? Size.little : Size.big,
|
|
startDelta + (index - 1) * 10
|
|
);
|
|
\end{minted}
|
|
\dots lors de l'animation des piques\dots
|
|
\begin{minted}[autogobble,linenos,firstnumber=last]{js}
|
|
if (ennemy.data.position.x <= -10) {
|
|
ennemy.data.position.x = ennemy.startPos + Math.random() * 20;
|
|
}
|
|
\end{minted}
|
|
\end{minipage}
|
|
\caption{Création d'un pique et retéléportation lors de la sortie de l'écran, \texttt{Env.js}}
|
|
\label{cod:spade}
|
|
\end{figure}
|
|
|
|
\subsection{Saut du joueur}
|
|
Dans le cas d'un joueur humain, on écoute via \texttt{addEventListener}
|
|
l'événement de la touche \textit{espace} pressée. Dans le cas de la démo, on utilise
|
|
un évènement interne \textit{nommé \texttt{jumpKey}}.
|
|
|
|
Les deux événements sont enregistrés et lanceront la fonction de saut.
|
|
|
|
\begin{figure}[h]
|
|
\centering
|
|
\begin{minipage}{0.7\textwidth}
|
|
\begin{minted}[autogobble,linenos]{js}
|
|
if (demo) {
|
|
addEventListener("jumpKey", (e) => player.controlUser(e.detail));
|
|
} else {
|
|
addEventListener("keypress", player.controlUser);
|
|
}
|
|
\end{minted}
|
|
\end{minipage}
|
|
\caption{Enregistrement des événements de saut, \texttt{Game.js}}
|
|
\label{cod:eventjump}
|
|
\end{figure}
|
|
|
|
La fonction \texttt{controlUser} définit le point de saut maximal ainsi que la
|
|
rotation du joueur finale.
|
|
|
|
\begin{figure}[h]
|
|
\centering
|
|
\begin{minipage}{0.7\textwidth}
|
|
\begin{minted}[autogobble,linenos]{js}
|
|
controlUser = (key) => {
|
|
if (key.code == "Space" && !this.movementData.state) {
|
|
this.movementData.changeRotation(
|
|
this.data.rotation.y - Math.PI / 2
|
|
);
|
|
this.movementData.changeJump(
|
|
3 * this.data.position.y + Math.PI / 2
|
|
);
|
|
this.movementData.changeState();
|
|
}
|
|
};
|
|
\end{minted}
|
|
\end{minipage}
|
|
\caption{Définition du saut max et de la rotation finale, \texttt{Player.js}}
|
|
\label{cod:defjumprot}
|
|
\end{figure}
|
|
|
|
Les coordonnées de transition (le saut et la descente) ainsi que l'angle sont calculées lors de l'animation du joueur, dans la fonction \texttt{animation} de la
|
|
classe \texttt{Player}.
|
|
|
|
\subsubsection[Démo]{Cas de la démonstration}
|
|
On vérifie si un ennemi est devant lors de l'animation du joueur, si c'est le
|
|
cas alors on saute.
|
|
|
|
\begin{figure}[h]
|
|
\centering
|
|
\begin{minipage}{0.7\textwidth}
|
|
\begin{minted}[autogobble,linenos]{js}
|
|
if (demo) {
|
|
listEnnemies.forEach((ennemy) => {
|
|
const pos = ennemy.position.x - joueur.position.x;
|
|
// Aléatoire entre 0.5 (proche du pique) et 2 (loin du pique)
|
|
if (pos > 0 && pos < Math.random() * 1.5 + 0.5) {
|
|
// Événement "jumpKey"
|
|
dispatchEvent(jumpDemo);
|
|
}
|
|
});
|
|
}
|
|
\end{minted}
|
|
\end{minipage}
|
|
\caption{Vérification IA : saut automatique, \texttt{Player.js}}
|
|
\label{cod:ia}
|
|
\end{figure}
|
|
|
|
\subsection{Collisions}
|
|
À chaque frame du jeu, le joueur vérifie qu'il n'entre pas en collision avec
|
|
les ennemies via du
|
|
\href{https://threejs.org/docs/#api/en/core/Raycaster}{ray casting}.
|
|
|
|
\begin{figure}[h]
|
|
\centering
|
|
\begin{minipage}{0.7\textwidth}
|
|
\begin{minted}[autogobble,linenos]{js}
|
|
const rc = new THREE.Raycaster(
|
|
this.data.position,
|
|
directionVector.normalize()
|
|
);
|
|
|
|
// Contient les éléments en collisions avec le joueur
|
|
const collisionResults = rc.intersectObjects(listEnnemies, false);
|
|
if (
|
|
collisionResults.length > 0 &&
|
|
collisionResults[0].distance < directionVector.length()
|
|
) {
|
|
return true;
|
|
}
|
|
return false;
|
|
\end{minted}
|
|
\end{minipage}
|
|
\caption{Vérification des collisions, \texttt{Player.js}}
|
|
\label{cod:raycast}
|
|
\end{figure}
|
|
|
|
\subsection{Fin de jeu}
|
|
Le jeu se termine dès que le joueur entre en collision avec un ennemi. Quand
|
|
c'est le cas, un menu de fin apparaît au-dessus du jeu, invitant le joueur à
|
|
rejouer. Peu importe si c'est une démo ou non.
|
|
|
|
\newpage
|
|
\appendix
|
|
\section*{Appendix}
|
|
\listoffigures
|
|
|
|
\end{document}
|