Dans ce didacticiel, nous explorerons une approche permettant de créer un jeu de sokoban ou de pousseur de caisses à l’aide de la logique à base de mosaïques et d’un tableau à deux dimensions permettant de stocker des données de niveau. Nous utilisons Unity pour le développement avec C # comme langage de script. Veuillez télécharger les fichiers sources fournis avec ce tutoriel pour les suivre.
Parmi nous, il y en a peut-être peu qui n'ont peut-être pas joué à une variante du jeu Sokoban. La version originale peut même être plus ancienne que certains d'entre vous. S'il vous plaît consulter la page du wiki pour quelques détails. Nous avons essentiellement un personnage ou un élément contrôlé par l'utilisateur qui doit pousser des caisses ou des éléments similaires sur sa tuile de destination..
Le niveau consiste en une grille carrée ou rectangulaire de tuiles où une tuile peut être une marche ou une marche. Nous pouvons marcher sur les dalles praticables et y placer les caisses. Les tuiles spéciales pour les piétons seraient marquées comme des tuiles de destination, c’est là que la caisse devrait reposer avant de terminer le niveau. Le personnage est généralement contrôlé à l'aide d'un clavier. Une fois que toutes les caisses ont atteint une tuile de destination, le niveau est terminé.
Le développement basé sur les tuiles signifie essentiellement que notre jeu est composé d'un certain nombre de tuiles réparties de manière prédéterminée. Un élément de données de niveau représentera la manière dont les tuiles devraient être réparties pour créer notre niveau. Dans notre cas, nous utiliserons une grille à carreaux carrés. Vous pouvez en savoir plus sur les jeux basés sur des tuiles ici sur Envato Tuts+.
Voyons comment nous avons organisé notre projet Unity pour ce tutoriel..
Pour ce projet de didacticiel, nous n'utilisons aucun actif d'art externe, mais nous allons utiliser les primitives de sprite créées avec la dernière version d'Unity 2017.1. L'image ci-dessous montre comment créer différentes images-objets dans Unity..
Nous allons utiliser le Carré sprite pour représenter une seule tuile dans notre grille de niveau sokoban. Nous allons utiliser le Triangle sprite pour représenter notre personnage, et nous allons utiliser le Cercle sprite pour représenter une caisse, ou dans ce cas une balle. Les carreaux au sol normaux sont blancs, tandis que les carreaux de destination ont une couleur différente pour se démarquer.
Nous allons représenter nos données de niveau sous la forme d'un tableau à deux dimensions qui fournit la corrélation parfaite entre les éléments logiques et visuels. Nous utilisons un simple fichier texte pour stocker les données de niveau, ce qui nous permet de modifier facilement le niveau en dehors de Unity ou de modifier les niveaux simplement en modifiant les fichiers chargés. le Ressources dossier a un niveau
fichier texte, qui a notre niveau par défaut.
1,1,1,1,1,1,1 1,3,1, -1,1,0,1 -1,0,1,2,1,1, -1 1,1,1,3, 1,3,1 1,1,0, -1,1,1,1
Le niveau comporte sept colonnes et cinq lignes. Une valeur de 1
signifie que nous avons une tuile au sol à cette position. Une valeur de -1
signifie qu'il s'agit d'une tuile non praticable, alors qu'une valeur de 0
signifie que c'est une tuile de destination. La valeur 2
représente notre héros, et 3
représente une balle pouvant être poussée. En regardant simplement les données de niveau, nous pouvons visualiser à quoi ressemblerait notre niveau..
Pour garder les choses simples, et comme ce n’est pas une logique très compliquée, nous n’avons qu’un seul Sokoban.cs
fichier de script pour le projet, et il est attaché à la caméra de la scène. Veuillez le garder ouvert dans votre éditeur pendant que vous suivez le reste du tutoriel..
Les données de niveau représentées par le tableau 2D sont non seulement utilisées pour créer la grille initiale, mais également tout au long du jeu pour suivre les changements de niveau et la progression de la partie. Cela signifie que les valeurs actuelles ne sont pas suffisantes pour représenter certains états de niveau pendant le jeu..
Chaque valeur représente l'état de la tuile correspondante dans le niveau. Nous avons besoin de valeurs supplémentaires pour représenter une balle sur la tuile de destination et le héros sur la tuile de destination, qui sont respectivement: -3
et -2
. Ces valeurs peuvent être n'importe quelle valeur que vous attribuez dans le script de jeu, mais pas nécessairement les mêmes valeurs que celles utilisées ici..
La première étape consiste à charger nos données de niveau dans un tableau 2D à partir du fichier texte externe. Nous utilisons le ParseLevel
méthode pour charger le chaîne
valeur et diviser pour peupler notre levelData
Tableau 2D.
void ParseLevel () TextAsset textFile = Resources.Load (levelName) as TextAsset; string [] lines = textFile.text.Split (new [] '\ r', '\ n', System.StringSplitOptions.RemoveEmptyEntries); // scindé par une nouvelle ligne, retourne string [] nums = lines [0] .Split (new [] ','); // divisé en lignes = lignes.longueur; // nombre de lignes cols = nums.Length; // nombre de colonnes levelData = new int [lignes, colonnes]; pour (int i = 0; i < rows; i++) string st = lines[i]; nums = st.Split(new[] ',' ); for (int j = 0; j < cols; j++) int val; if (int.TryParse (nums[j], out val)) levelData[i,j] = val; else levelData[i,j] = invalidTile;
Lors de l'analyse, nous déterminons le nombre de lignes et de colonnes de notre niveau au fur et à mesure que nous peuplons notre levelData
.
Une fois que nous avons nos données de niveau, nous pouvons dessiner notre niveau à l'écran. Nous utilisons la méthode CreateLevel pour faire exactement cela.
void CreateLevel () // calcule le décalage pour aligner le niveau entier sur la scène middle middleOffset.x = cols * tileSize * 0.5f-tileSize * 0.5f; middleOffset.y = rows * tileSize * 0.5f-tileSize * 0.5f ;; Tuile GameObject; SpriteRenderer sr; Balle GameObject; int destinationCount = 0; pour (int i = 0; i < rows; i++) for (int j = 0; j < cols; j++) int val=levelData[i,j]; if(val!=invalidTile)//a valid tile tile = new GameObject("tile"+i.ToString()+"_"+j.ToString());//create new tile tile.transform.localScale=Vector2.one*(tileSize-1);//set tile size sr = tile.AddComponent(); // ajout d'un rendu d'image-objet sr.sprite = tileSprite; // attribuer une image-objet à la mosaïque tile.transform.position = GetScreenPointFromLevelIndices (i, j); // lieu dans la scène en fonction des indices de niveau if (val == destinationTile) // s'il s'agit d'une mosaïque de destination, donnez une couleur différente sr.color = destinationColor; destinationCount ++; // compte les destinations else if (val == heroTile) // la tuile héros Hero = new GameObject ("hero"); hero.transform.localScale = Vector2.one * (tileSize-1); sr = hero.AddComponent (); sr.sprite = heroSprite; sr.sortingOrder = 1; // le héros doit être sur la tuile du sol sr.color = Color.red; hero.transform.position = GetScreenPointFromLevelIndices (i, j); occupants.Add (héros, nouveau vecteur2 (i, j)); // stocke les indices de niveau du héros dans dict else if (val == ballTile) // balle tuile balleCompte ++; // incrémente le nombre de balles dans une balle de niveau = new GameObject ("ball" + ballCount.ToString ()); ball.transform.localScale = Vector2.one * (tileSize-1); sr = ball.AddComponent (); sr.sprite = ballSprite; sr.sortingOrder = 1; // la balle doit être au-dessus du pavé de sol sr.color = Color.black; ball.transform.position = GetScreenPointFromLevelIndices (i, j); occupants.Add (ball, new Vector2 (i, j)); // stocke les indices de niveau de ball dans dict if (ballCount> destinationCount) Debug.LogError ("il y a plus de balles que de destinations");
Pour notre niveau, nous avons défini un TailleTuile
valeur de 50
, qui est la longueur du côté d'une tuile carrée dans notre grille de niveau. Nous parcourons notre tableau 2D et déterminons la valeur stockée dans chacun des je
et j
indices du tableau. Si cette valeur n'est pas un invalidTile
(-1) alors on crée un nouveau GameObject
nommé tuile
. Nous attachons un SpriteRenderer
composant à tuile
et assigner le correspondant Lutin
ou Couleur
en fonction de la valeur à l'index du tableau.
En plaçant le héros
ou la ballon
, nous devons d'abord créer une tuile de terre, puis créer ces tuiles. Comme le héros et la balle doivent superposer la tuile au sol, nous leur donnons SpriteRenderer
un plus haut ordre de tri
. Toutes les tuiles se voient attribuer un échelle locale
de TailleTuile
donc ils sont 50x50
dans notre scène.
Nous gardons une trace du nombre de balles dans notre scène en utilisant le ballCount
variable, et il devrait y avoir un nombre identique ou supérieur de tuiles de destination dans notre niveau pour permettre l’achèvement du niveau. La magie se produit dans une seule ligne de code où nous déterminons la position de chaque tuile en utilisant le GetScreenPointFromLevelIndices (int row, int col)
méthode.
//… tile.transform.position = GetScreenPointFromLevelIndices (i, j); // place dans la scène en fonction des index de niveau //… Vector2 GetScreenPointFromLevelIndices (int row, int col) // conversion d'index en valeurs de position, col détermine x & row détermine y retourne le nouveau Vector2 (col * tileSize-middleOffset.x, row * -tileSize + middleOffset.y);
La position mondiale d’une tuile est déterminée en multipliant les indices de niveau par le TailleTuile
valeur. le middleOffset
variable est utilisé pour aligner le niveau au milieu de l'écran. Notez que le rangée
la valeur est multipliée par une valeur négative afin de supporter l'inversion y
axe dans l'unité.
Maintenant que nous avons affiché notre niveau, passons à la logique de jeu. Nous devons écouter la saisie par la touche utilisateur et déplacer le curseur. héros
basé sur l'entrée. L’appui sur la touche détermine la direction requise du mouvement, et le héros
doit être déplacé dans cette direction. Il existe différents scénarios à considérer une fois que nous avons déterminé la direction requise du mouvement. Disons que la tuile à côté de héros
dans cette direction est tuileK.
Si la position de tileK est en dehors de la grille, nous n'avons rien à faire. Si tileK est valide et peut être parcouru à pied, nous devons nous déplacer héros
à cette position et mettre à jour notre levelData
tableau. Si tileK a un ballon, alors nous devons considérer le prochain voisin dans la même direction, disons tuileL.
Dans le cas où tuileL est une tuile accessible à pied et non occupée, faut-il déplacer le héros
et la balle à tileK à tileK et tileL respectivement. Après un mouvement réussi, nous devons mettre à jour le levelData
tableau.
La logique ci-dessus signifie que nous devons savoir quelle tuile notre héros
est actuellement à. Nous devons également déterminer si une certaine tuile a une balle et devrait y avoir accès..
Pour faciliter cela, nous utilisons un dictionnaire
appelé les occupants
qui stocke un GameObject
comme clé et ses indices de tableau stockés sous Vecteur2
comme valeur. dans le CreateLevel
méthode, nous peuplons les occupants
quand on crée héros
ou balle. Une fois le dictionnaire rempli, nous pouvons utiliser le GetOccupantAtPosition
pour récupérer le GameObject
à un index de tableau donné.
dictionnaireoccupants; // référence aux balles & héros //… occupants.Add (héros, nouveau vecteur2 (i, j)); // stocke les index de niveau du héros dans dict //… occupants.Add (balle, nouveau vecteur2 (i , j)); // stocke les indices de niveau de la balle dans dict //… private GameObject GetOccupantAtPosition (Vector2 heroPos) // parcourt les occupants pour trouver la balle à la position donnée GameObject ball; foreach (KeyValuePair paire dans les occupants) if (pair.Value == heroPos) ball = pair.Key; balle de retour; return null;
le Est occupé
méthode détermine si le levelData
la valeur aux indices fournis représente une balle.
private bool IsOccupied (Vector2 objPos) // vérifie s'il y a une balle à la position de tableau donnée (levelData [(int) objPos.x, (int) objPos.y] == ballTile || levelData [(int) objPos. x, (int) objPos.y] == ballOnDestinationTile);
Nous avons également besoin d'un moyen de vérifier si une position donnée se trouve dans notre grille et si cette tuile est praticable. le IsValidPosition
méthode vérifie les indices de niveau passés en tant que paramètres pour déterminer s’il se situe ou non dans nos dimensions de niveau. Il vérifie également si nous avons un invalidTile
comme cet indice dans le levelData
.
private bool IsValidPosition (Vector2 objPos) // vérifie si les index donnés entrent dans les dimensions du tableau if (objPos.x> -1 && objPos.x-1 && objPos.y Répondre à la saisie de l'utilisateur
dans le
Mettre à jour
méthode de notre script de jeu, nous vérifions pour l'utilisateurKeyUp
événements et comparer avec nos clés d'entrée stockées dans leuserInputKeys
tableau. Une fois que la direction requise du mouvement est déterminée, nous appelons leTryMoveHero
méthode avec la direction en paramètre.void Update () if (gameOver) return; ApplyUserInput (); // vérifier et utiliser les entrées utilisateur pour déplacer le héros et les balles private void ApplyUserInput () if (Input.GetKeyUp (userInputKeys [0])) TryMoveHero (0); // up else if (Input. GetKeyUp (userInputKeys [1])) TryMoveHero (1); // right else if (Input.GetKeyUp (userInputKeys [2])) TryMoveHero (2); // bas autrement if (Input.GetKeyUp (userInputKeys [2]). 3])) TryMoveHero (3); // leftle
TryMoveHero
La méthode est celle où notre logique de jeu principale expliquée au début de cette section est mise en œuvre. Veuillez suivre attentivement la méthode suivante pour voir comment la logique est implémentée comme expliqué ci-dessus..TryMoveHero (int direction) Vector2 heroPos; Vector2 oldHeroPos; Vector2 nextPos; occupants.TryGetValue (héros, hors oldHeroPos); heroPos = GetNextPositionAlong (oldHeroPos, direction); // trouve la position du tableau suivant dans une direction donnée si (IsValidPosition (heroPos)) // vérifie si cette position est valide et tombe dans le tableau de niveaux si (! IsOccupied (heroPos)) // vérifie si elle est occupée par une balle // déplace le héros RemoveOccupant (oldHeroPos); // réinitialise les données de l'ancien niveau à l'ancienne position hero.transform.position = GetScreenPointFromLevelIndices ((int) heroPos.x, (int) heroPos.y ) occupants [héros] = heroPos; if (levelData [(int) heroPos.x, (int) heroPos.y] == groundTile) // se déplaçant sur une tuile de sol levelData [(int) heroPos.x, (int) heroPos.y] = heroTile; else if (levelData [(int) heroPos.x, (int) heroPos.y] == destinationTile) // passage sur une tuile de destination levelData [(int) heroPos.x, (int) heroPos.y] = heroOnDestinationTile ; else // nous avons une balle à côté de hero, vérifiez si elle est vide de l'autre côté de la balle nextPos = GetNextPositionAlong (heroPos, direction); if (IsValidPosition (nextPos)) if (! IsOccupied (nextPos)) // nous avons trouvé un voisin vide; nous devons donc déplacer la balle et le héros. (ball == null) Debug.Log ("pas de balle"); RemoveOccupant (heroPos); // ball doit d'abord être déplacé avant de déplacer le héros ball.transform.position = GetScreenPointFromLevelIndices ((int) nextPos.x, (int) nextPos.y); occupants [balle] = nextPos; if (levelData [(int) nextPos.x, (int) nextPos.y] == groundTile) levelData [(int) nextPos.x, (int) nextPos.y] = ballTile; else if (levelData [(int) nextPos.x, (int) nextPos.y] == destinationTile) levelData [(int) nextPos.x, (int) nextPos.y] = ballOnDestinationTile; RemoveOccupant (oldHeroPos); // déplace maintenant le héros hero.transform.position = GetScreenPointFromLevelIndices ((int) heroPos.x, (int) heroPos.y); occupants [héros] = heroPos; if (levelData [(int) heroPos.x, (int) heroPos.y] == groundTile) levelData [(int) heroPos.x, (int) heroPos.y] = heroTile; else if (levelData [(int) heroPos.x, (int) heroPos.y] == destinationTile) levelData [(int) heroPos.x, (int) heroPos.y] = heroOnDestinationTile; CheckCompletion (); // vérifie si toutes les balles ont atteint la destinationAfin d’obtenir la position suivante dans une certaine direction sur la base d’une position fournie, nous utilisons le
GetNextPositionAlong
méthode. Il s’agit simplement d’augmenter ou de décrémenter l’un ou l’autre des indices en fonction de la direction.private Vector2 GetNextPositionAlong (Vector2 objPos, direction int) commutateur (direction) cas 0: objPos.x- = 1; // up pause; cas 1: objPos.y + = 1; // pause droite; cas 2: objPos.x + = 1; // down break; cas 3: objPos.y- = 1; // pause à gauche; return objPos;Avant de déplacer le héros ou la balle, nous devons effacer leur position actuellement occupée dans
levelData
tableau. Ceci est fait en utilisant leRemoveOccupant
méthode.espace privé privé RemoveOccupant (Vector2 objPos) if (levelData [(int) objPos.x, (int) objPos.y] == heroTile || levelData [(int) objPos.x, (int) objPos.y] == ballTile ) levelData [(int) objPos.x, (int) objPos.y] = balle au sol; // balle se déplaçant depuis la tuile au sol else if (levelData [(int) objPos.x, (int) objPos.y] == heroOnDestinationTile) levelData [(int) objPos.x, (int) objPos.y] = destinationTile; // héros se déplaçant de la mosaïque de destination else if (levelData [(int) objPos.x, (int) objPos.y] = = ballOnDestinationTile) levelData [(int) objPos.x, (int) objPos.y] = destinationTile; // la balle se déplace de la tuile de destinationSi on trouve un
heroTile
ouballTile
à l'index donné, nous devons le mettre àcarrelage
. Si on trouve unheroOnDestinationTile
ouballOnDestinationTile
alors nous devons le régler surdestinationTile
.Niveau d'achèvement
Le niveau est terminé lorsque toutes les balles sont à leur destination.
Après chaque mouvement réussi, nous appelons le
CheckCompletion
méthode pour voir si le niveau est terminé. Nous parcourons notrelevelData
tableau et compter le nombre deballOnDestinationTile
occurrences. Si ce nombre est égal à notre nombre total de balles déterminé parballCount
, le niveau est complet.private void CheckCompletion () int ballsOnDestination = 0; pour (int i = 0; i < rows; i++) for (int j = 0; j < cols; j++) if(levelData[i,j]==ballOnDestinationTile) ballsOnDestination++; if(ballsOnDestination==ballCount) Debug.Log("level complete"); gameOver=true;Conclusion
Ceci est une implémentation simple et efficace de la logique sokoban. Vous pouvez créer vos propres niveaux en modifiant le fichier texte ou en créant un nouveau et en modifiant le
levelName
variable pour pointer vers votre nouveau fichier texte.L'implémentation actuelle utilise le clavier pour contrôler le héros. Je vous invite à essayer de modifier le contrôle en fonction de la prise en charge afin que nous puissions prendre en charge les périphériques tactiles. Cela impliquerait également l'ajout de chemins de recherche en 2D si vous avez envie de toucher n'importe quelle tuile pour y conduire le héros..
Il y aura un tutoriel de suivi où nous explorerons comment le projet actuel peut être utilisé pour créer des versions isométriques et hexagonales de sokoban avec des modifications minimes..