Cette série expliquera comment créer un système physique simple et robuste pour un jeu de plateforme. Dans cette partie, nous examinerons les données de collision de personnages..
Le principe est le suivant: nous voulons créer un jeu de plateforme 2D avec une physique simple, robuste, réactive, précise et prévisible. Dans ce cas, nous ne voulons pas utiliser un gros moteur physique 2D, et cela pour plusieurs raisons:
Bien sûr, il existe de nombreux avantages à utiliser un moteur physique standard, comme par exemple être capable de configurer des interactions physiques complexes assez facilement, mais ce n'est pas ce dont nous avons besoin pour notre jeu..
Un moteur physique personnalisé aide le jeu à se sentir personnalisé, ce qui est vraiment important! Même si vous commencez avec une configuration relativement basique, la manière dont les choses bougeront et interagiront les unes avec les autres sera toujours influencée par vos propres règles, plutôt que par celles de quelqu'un d'autre. Allons-y!
Commençons par définir le type de formes que nous utiliserons dans notre physique. L'une des formes les plus élémentaires que nous puissions utiliser pour représenter un objet physique dans un jeu est une boîte englobante alignée (Axis Aligned Bounding Box ou AABB). AABB est fondamentalement un rectangle sans rotation.
Dans beaucoup de jeux de plateforme, les AABB suffisent pour approcher le corps de chaque objet de la partie. Ils sont extrêmement efficaces, car il est très facile de calculer un chevauchement entre AABB et nécessite très peu de données. Pour décrire un AABB, il suffit de connaître son centre et sa taille..
Sans plus tarder, créons une structure pour notre AABB.
structure publique AABB
Comme mentionné précédemment, il suffit de deux vecteurs en ce qui concerne les données. le premier sera le centre de l'AABB et le second la demi-taille. Pourquoi demi taille? La plupart du temps pour les calculs, nous aurons quand même besoin de la moitié de la taille. Au lieu de la calculer à chaque fois, nous la mémoriserons simplement au lieu de la taille complète..
structure publique AABB centre public Vector2; public Vector2 halfSize;
Commençons par ajouter un constructeur, il est donc possible de créer la structure avec des paramètres personnalisés.
public AABB (Vector2 center, Vector2 halfSize) this.center = center; this.halfSize = halfSize;
Avec cela, nous pouvons créer les fonctions de vérification des collisions. Commençons par vérifier si deux AABB se rencontrent. C’est très simple: il suffit de voir si la distance entre les centres de chaque axe est inférieure à la somme des demi-pointures..
Chevauchement de bool public (AABB autre) if (Mathf.Abs (center.x - other.center.x)> halfSize.x + other.halfSize.x) return false; if (Mathf.Abs (center.y - other.center.y)> halfSize.y + other.halfSize.y) renvoie la valeur false; retourne vrai;
Voici une image montrant cette vérification sur l’axe des x; l'axe y est vérifié de la même manière.
Comme vous pouvez le constater, si la somme des demi-tailles devait être inférieure à la distance entre les centres, aucun chevauchement ne serait possible. Notez que dans le code ci-dessus, nous pouvons échapper à la vérification de collision si nous constatons que les objets ne se chevauchent pas sur le premier axe. Le chevauchement doit exister sur les deux axes si les AABB doivent entrer en collision dans un espace 2D.
Commençons par créer une classe pour un objet influencé par la physique du jeu. Plus tard, nous utiliserons cela comme base pour un objet de joueur réel. Appelons cette classe MovingObject.
Classe publique MovingObject
Maintenant remplissons cette classe avec les données. Nous aurons besoin de beaucoup d'informations pour cet objet:
La position, la vitesse et l'échelle sont des vecteurs 2D.
classe publique MovingObject public Vector2 mOldPosition; public Vector2 mPosition; public Vector2 mOldSpeed; public Vector2 mSpeed; public Vector2 mScale;
Ajoutons maintenant le AABB et le décalage. Le décalage est nécessaire afin que nous puissions librement faire correspondre l'AABB au sprite de l'objet.
public AABB mAABB; public Vector2 mAABBOffset;
Et enfin, déclarons les variables qui indiquent l’état de position de l’objet, que ce soit au sol, près d’un mur ou au plafond. Celles-ci sont très importantes car elles nous permettront de savoir si nous pouvons sauter ou si, par exemple, nous devons jouer un son après avoir heurté un mur..
public bool mPushedRightWall; public bool mPushesRightWall; public bool mPushedLeftWall; public bool mPushesLeftWall; public bool mWasOnGround; public bool mOnGround; public bool mWasAtCeiling; public bool mAtCeiling;
Ce sont les bases. Créons maintenant une fonction qui mettra à jour l'objet. Pour l'instant, nous n'allons pas tout configurer, mais juste assez pour pouvoir commencer à créer des contrôles de base pour les personnages..
public void UpdatePhysics ()
La première chose à faire ici est de sauvegarder les données de la trame précédente dans les variables appropriées..
public void UpdatePhysics () mOldPosition = mPosition; mOldSpeed = mSpeed; mWasOnGround = mOnGround; mPushedRightWall = mPushesRightWall; mPushedLeftWall = mPushesLeftWall; mWasAtCeiling = mAtCeiling;
Maintenant mettons à jour la position en utilisant la vitesse actuelle.
mPosition + = mSpeed * Time.deltaTime;
Et juste pour le moment, faisons en sorte que si la position verticale est inférieure à zéro, nous supposons que le personnage est au sol. Ceci est juste pour le moment, nous pouvons donc configurer les contrôles du personnage. Plus tard, nous ferons une collision avec un tilemap.
si (mPosition.y < 0.0f) mPosition.y = 0.0f; mOnGround = true; else mOnGround = false;
Après cela, nous devons également mettre à jour le centre de l’AABB afin qu’il corresponde à la nouvelle position..
mAABB.center = mPosition + mAABBOffset;
Pour le projet de démonstration, j'utilise Unity et pour mettre à jour la position de l'objet, il doit être appliqué au composant de transformation. Faisons-le également. La même chose doit être faite pour la balance.
mTransform.position = new Vector3 (Mathf.Round (mPosition.x), Mathf.Round (mPosition.y), - 1.0f); mTransform.localScale = new Vector3 (mScale.x, mScale.y, 1.0f);
Comme vous pouvez le constater, la position rendue est arrondie. Cela permet de s'assurer que le caractère rendu est toujours accroché à un pixel.
Maintenant que notre classe MovingObject de base est terminée, nous pouvons commencer par jouer avec le mouvement du personnage. Après tout, c'est une partie très importante du jeu, et vous pouvez le faire presque tout de suite. Inutile de vous plonger trop profondément dans les systèmes de jeu, et ce sera prêt lorsque nous aurons besoin de tester notre personnage- collisions de carte.
Commençons par créer une classe Character et dériver de la classe MovingObject..
Classe publique Character: MovingObject
Nous aurons besoin de gérer quelques choses ici. Tout d’abord, les entrées-faisons une énumération qui couvrira tous les contrôles pour le personnage. Créons-le dans un autre fichier et appelons-le KeyInput.
public enum KeyInput GoLeft = 0, GoRight, GoDown, Jump, Count
Comme vous pouvez le constater, notre personnage peut se déplacer à gauche, à droite, en bas et sauter. Descendre ne fonctionnera que sur des plates-formes à sens unique, lorsque nous voulons passer à travers elles.
Déclarons maintenant deux tableaux dans la classe Character, un pour les entrées du cadre actuel et un autre pour le cadre précédent. Selon le jeu, cette configuration peut avoir plus de sens. Généralement, au lieu de sauvegarder l’état de la clé dans un tableau, il est vérifié à la demande à l’aide de fonctions spécifiques du moteur ou de la structure. Cependant, avoir un tableau qui n'est pas strictement lié à une entrée réelle peut être bénéfique, si par exemple nous voulons simuler des appuis sur les touches.
protégé bool [] mInputs; protégé bool [] mPrevInputs;
Ces tableaux seront indexés par l'énumération KeyInput. Pour utiliser facilement ces tableaux, créons quelques fonctions qui nous aideront à vérifier une clé spécifique.
protected bool Released (clé KeyInput) return (! mInputs [(int) clé] && mPrevInputs [(int) clé])); protégé bool KeyState (clé KeyInput) return (mInputs [clé (int)]]); protégé booléen Pressé (touche KeyInput) return (mInputs [(int) key] &&! mPrevInputs [(int) key]);
Rien de spécial ici: nous voulons savoir si une touche a été pressée, relâchée, activée ou désactivée..
Créons maintenant une autre énumération qui contiendra tous les états possibles du personnage..
Énumération publique CharacterState Stand, Walk, Jump, GrabLedge,;
Comme vous pouvez le constater, notre personnage peut rester immobile, marcher, sauter ou attraper un rebord. Maintenant que cela est fait, nous devons ajouter des variables telles que la vitesse de saut, la vitesse de déplacement et l'état actuel.
public CharacterState mCurrentState = CharacterState.Stand; public float mJumpSpeed; public float mWalkSpeed;
Bien sûr, il faut plus de données ici, comme le caractère de sprite, mais leur apparence dépend beaucoup du type de moteur que vous allez utiliser. Depuis que j'utilise Unity, je vais utiliser une référence à un animateur pour m'assurer que l'image-objet lit l'animation pour un état approprié..
Bon, maintenant nous pouvons commencer le travail sur la boucle de mise à jour. Ce que nous ferons ici dépendra de l'état actuel du personnage..
public void CharacterUpdate () switch (mCurrentState) case CharacterState.Stand: break; case CharacterState.Walk: break; case CharacterState.Jump: break; case CharacterState.GrabLedge: break;
Commençons par préciser ce qui devrait être fait lorsque le personnage ne bouge pas - dans l’état du stand. Tout d'abord, la vitesse doit être réglée à zéro.
case CharacterState.Stand: mSpeed = Vector2.zero; Pause;
Nous voulons aussi montrer le sprite approprié pour l'état.
case CharacterState.Stand: mSpeed = Vector2.zero; Manimator.Play ("Stand"); Pause;
Maintenant, si le personnage n'est pas sur le sol, il ne peut plus se tenir, nous devons donc changer d'état pour sauter.
case CharacterState.Stand: mSpeed = Vector2.zero; Manimator.Play ("Stand"); if (! mOnGround) mCurrentState = CharacterState.Jump; Pause; Pause;
Si la touche GoLeft ou GoRight est enfoncée, nous devons changer notre état pour marcher..
case CharacterState.Stand: mSpeed = Vector2.zero; Manimator.Play ("Stand"); if (! mOnGround) mCurrentState = CharacterState.Jump; Pause; if (KeyState (KeyInput.GoRight)! = KeyState (KeyInput.GoLeft)) mCurrentState = CharacterState.Walk; pause pause;
Si la touche de saut est enfoncée, nous voulons régler la vitesse verticale sur la vitesse de saut et changer l'état pour sauter..
if (KeyState (KeyInput.GoRight)!! = KeyState (KeyInput.GoLeft)) mCurrentState = CharacterState.Walk; Pause; else if (KeyState (KeyInput.Jump)) mSpeed.y = mJumpSpeed; mCurrentState = CharacterState.Jump; Pause;
Ça va être pour cet état, au moins pour l'instant.
Créons maintenant une logique de déplacement sur le terrain et commençons immédiatement à jouer l’animation de marche..
case CharacterState.Walk: mAnimator.Play ("Walk"); Pause;
Ici, si nous n'appuyons pas sur le bouton gauche ou droit, ou si nous appuyons sur les deux, nous voulons revenir à l'état immobile.
if (KeyState (KeyInput.GoRight) == KeyState (KeyInput.GoLeft)) mCurrentState = CharacterState.Stand; mSpeed = Vector2.zero; Pause;
Si vous appuyez sur la touche GoRight, vous devez définir la vitesse horizontale sur mWalkSpeed et vous assurer que l'image-objet est correctement mise à l'échelle. L'échelle horizontale doit être modifiée si vous souhaitez inverser l'image-objet horizontalement..
Nous devrions également nous déplacer que s'il n'y a pas d'obstacle à venir. Si mPushesRightWall est défini sur true, la vitesse horizontale doit alors être définie sur zéro si nous allons à droite..
if (KeyState (KeyInput.GoRight) == KeyState (KeyInput.GoLeft)) mCurrentState = CharacterState.Stand; mSpeed = Vector2.zero; Pause; else if (KeyState (KeyInput.GoRight)) if (mPushesRightWall) mSpeed.x = 0.0f; sinon mSpeed.x = mWalkSpeed; mScale.x = Mathf.Abs (mScale.x); else if (KeyState (KeyInput.GoLeft)) if (mPushesLeftWall) mSpeed.x = 0.0f; sinon mSpeed.x = -mWalkSpeed; mScale.x = -Mathf.Abs (mScale.x);
Nous devons également gérer le côté gauche de la même manière.
Comme nous l'avons fait pour l'état debout, nous devons voir si un bouton de saut est enfoncé et définir la vitesse verticale si c'est le cas..
if (KeyState (KeyInput.Jump)) mSpeed.y = mJumpSpeed; mAudioSource.PlayOneShot (mJumpSfx, 1.0f); mCurrentState = CharacterState.Jump; Pause;
Sinon, si le personnage n'est pas au sol, il doit changer d'état pour sauter également, mais sans addition de vitesse verticale, il tombe donc tout simplement..
if (KeyState (KeyInput.Jump)) mSpeed.y = mJumpSpeed; mAudioSource.PlayOneShot (mJumpSfx, 1.0f); mCurrentState = CharacterState.Jump; Pause; else if (! mOnGround) mCurrentState = CharacterState.Jump; Pause;
Voilà pour la marche. Passons à l'état de saut.
Commençons par définir une animation appropriée pour le sprite.
Manimator.Play ("Jump");
Dans l'état Jump, nous devons ajouter de la gravité à la vitesse du personnage pour qu'il aille de plus en plus vite vers le sol.
mSpeed.y + = Constants.cGravity * Time.deltaTime;
Mais il serait judicieux d'ajouter une limite, afin que le personnage ne puisse pas tomber trop vite.
mSpeed.y = Mathf.Max (mSpeed.y, Constants.cMaxFallingSpeed);
Dans de nombreux jeux, lorsque le personnage est dans les airs, la maniabilité diminue, mais nous allons opter pour des commandes très simples et précises qui permettent une totale flexibilité dans les airs. Donc, si nous appuyons sur la touche GoLeft ou GoRight, le personnage se déplacera dans la direction tout en sautant aussi vite qu'il le ferait s'il était au sol. Dans ce cas, nous pouvons simplement copier la logique de mouvement de l'état de marche.
if (KeyState (KeyInput.GoRight) == KeyState (KeyInput.GoLeft)) mSpeed.x = 0.0f; else if (KeyState (KeyInput.GoRight)) if (mPushesRightWall) mSpeed.x = 0.0f; sinon mSpeed.x = mWalkSpeed; mScale.x = Mathf.Abs (mScale.x); else if (KeyState (KeyInput.GoLeft)) if (mPushesLeftWall) mSpeed.x = 0.0f; sinon mSpeed.x = -mWalkSpeed; mScale.x = -Mathf.Abs (mScale.x);
Enfin, nous allons augmenter le saut si le bouton de saut est enfoncé plus longtemps. Pour ce faire, nous allons faire le saut plus bas si le bouton de saut n’est pas enfoncé.
if (! KeyState (KeyInput.Jump) && mSpeed.y> 0.0f) mSpeed.y = Mathf.Min (mSpeed.y, Constants.cMinJumpSpeed);
Comme vous pouvez le constater, si la touche de saut n’est pas enfoncée et que la vitesse verticale est positive, nous fixons la vitesse à la valeur maximale de cMinJumpSpeed
(200 pixels par seconde). Cela signifie que si nous devions appuyer sur le bouton de saut, la vitesse du saut, au lieu d'être égale à mJumpSpeed
(410 par défaut), sera abaissé à 200, et donc le saut sera plus court.
Comme nous n'avons pas encore de géométrie de niveau, nous devrions ignorer l'implémentation de GrabLedge pour l'instant..
Une fois le cadre terminé, nous pouvons mettre à jour les entrées précédentes. Créons une nouvelle fonction pour cela. Tout ce que nous devons faire ici est de déplacer les valeurs d'état clés de la mInputs
tableau à la mPrevInputs
tableau.
void public UpdatePrevInputs () var count = (octet) KeyInput.Count; pour (octet i = 0; i < count; ++i) mPrevInputs[i] = mInputs[i];
À la toute fin de la fonction CharacterUpdate, il reste quelques choses à faire. La première consiste à mettre à jour la physique.
UpdatePhysics ();
Maintenant que la physique est mise à jour, nous pouvons voir si nous devrions jouer n'importe quel son. Nous voulons jouer un son lorsque le personnage heurte une surface, mais pour le moment, il ne peut toucher que le sol, car la collision avec tilemap n'est pas encore implémentée..
Vérifions si le personnage vient de tomber sur le sol. Il est très facile de le faire avec la configuration actuelle. Il suffit de regarder si le personnage est au sol à l'heure actuelle, mais ne l'était pas dans l'image précédente..
if (mOnGround &&! mWasOnGround) mAudioSource.PlayOneShot (mHitWallSfx, 0.5f);
Enfin, mettons à jour les entrées précédentes.
UpdatePrevInputs ();
En résumé, voici à quoi devrait ressembler la fonction CharacterUpdate, avec des différences mineures selon le type de moteur ou de structure utilisé..
void public CharacterUpdate () switch (mCurrentState) case CharacterState.Stand: mWalkSfxTimer = cWalkSfxTime; Manimator.Play ("Stand"); mSpeed = Vector2.zero; if (! mOnGround) mCurrentState = CharacterState.Jump; Pause; // si la touche gauche ou droite est enfoncée, mais pas les deux si (KeyState (KeyInput.GoRight)! = = KeyState (KeyInput.GoLeft)) mCurrentState = CharacterState.Walk; Pause; else if (KeyState (KeyInput.Jump)) mSpeed.y = mJumpSpeed; mAudioSource.PlayOneShot (mJumpSfx); mCurrentState = CharacterState.Jump; Pause; Pause; case CharacterState.Walk: mAnimator.Play ("Walk"); mWalkSfxTimer + = Time.deltaTime; if (mWalkSfxTimer> cWalkSfxTime) mWalkSfxTimer = 0.0f; mAudioSource.PlayOneShot (mWalkSfx); // si les deux touches, ni les touches gauche ni droite, sont enfoncées, arrêtez de marcher et restez debout si (KeyState (KeyInput.GoRight) == KeyState (KeyInput.GoLeft)) mCurrentState = CharacterState.Stand; mSpeed = Vector2.zero; Pause; else if (KeyState (KeyInput.GoRight)) if (mPushesRightWall) mSpeed.x = 0.0f; sinon mSpeed.x = mWalkSpeed; mScale.x = -Mathf.Abs (mScale.x); else if (KeyState (KeyInput.GoLeft)) if (mPushesLeftWall) mSpeed.x = 0.0f; sinon mSpeed.x = -mWalkSpeed; mScale.x = Mathf.Abs (mScale.x); // s'il n'y a aucune tuile sur laquelle marcher, tombez if (KeyState (KeyInput.Jump)) mSpeed.y = mJumpSpeed; mAudioSource.PlayOneShot (mJumpSfx, 1.0f); mCurrentState = CharacterState.Jump; Pause; else if (! mOnGround) mCurrentState = CharacterState.Jump; Pause; Pause; case CharacterState.Jump: mWalkSfxTimer = cWalkSfxTime; Manimator.Play ("Jump"); mSpeed.y + = Constants.cGravity * Time.deltaTime; mSpeed.y = Mathf.Max (mSpeed.y, Constants.cMaxFallingSpeed); if (! KeyState (KeyInput.Jump) && mSpeed.y> 0.0f) mSpeed.y = Mathf.Min (mSpeed.y, 200.0f); if (KeyState (KeyInput.GoRight) == KeyState (KeyInput.GoLeft)) mSpeed.x = 0.0f; else if (KeyState (KeyInput.GoRight)) if (mPushesRightWall) mSpeed.x = 0.0f; sinon mSpeed.x = mWalkSpeed; mScale.x = -Mathf.Abs (mScale.x); else if (KeyState (KeyInput.GoLeft)) if (mPushesLeftWall) mSpeed.x = 0.0f; sinon mSpeed.x = -mWalkSpeed; mScale.x = Mathf.Abs (mScale.x); // si nous touchons le sol if (mOnGround) // s'il n'y a pas d'état de changement de mouvement qui reste à l'état si (mInputs [(int) KeyInput.GoRight] == mInputs [(int) KeyInput.GoLeft]) mCurrentState = CharacterState .Supporter; mSpeed = Vector2.zero; mAudioSource.PlayOneShot (mHitWallSfx, 0.5f); else // soit on va vers la droite soit vers la gauche pour changer l'état en marche mCurrentState = CharacterState.Walk; mSpeed.y = 0.0f; mAudioSource.PlayOneShot (mHitWallSfx, 0.5f); Pause; case CharacterState.GrabLedge: break; UpdatePhysics (); si (!!!!!!!!) UpdatePrevInputs ();
Ecrivons une fonction Init pour le personnage. Cette fonction prendra les tableaux d’entrée comme paramètres. Nous les fournirons plus tard de la classe de gestionnaires. Autre que cela, nous devons faire des choses comme:
public void CharacterInit (bool [] entrées, bool [] prevInputs)
Nous allons utiliser quelques-unes des constantes définies ici.
public const float cWalkSpeed = 160.0f; public const float cJumpSpeed = 410.0f; public const float cMinJumpSpeed = 200.0f; public const float cHalfSizeY = 20.0f; public const float cHalfSizeX = 6.0f;
Dans le cas de la démo, on peut définir la position initiale à la position dans l'éditeur.
public void CharacterInit (bool [] entrées, bool [] prevInputs) mPosition = transform.position;
Pour l'AABB, nous devons définir le décalage et la demi-taille. Le décalage dans le cas du sprite de la démo doit être juste la moitié de la taille.
public void CharacterInit (bool [] entrées, bool [] prevInputs) mPosition = transform.position; mAABB.halfSize = new Vector2 (Constants.cHalfSizeX, Constants.cHalfSizeY); mAABBOffset.y = mAABB.halfSize.y;
Maintenant, nous pouvons nous occuper du reste des variables.
public void CharacterInit (bool [] entrées, bool [] prevInputs) mPosition = transform.position; mAABB.halfSize = new Vector2 (Constants.cHalfSizeX, Constants.cHalfSizeY); mAABBOffset.y = mAABB.halfSize.y; mInputs = input; mPrevInputs = prevInputs; mJumpSpeed = Constants.cJumpSpeed; mWalkSpeed = Constants.cWalkSpeed; mScale = Vector2.one;
Nous devons appeler cette fonction à partir du gestionnaire de jeu. Le gestionnaire peut être configuré de différentes manières, tout dépend des outils que vous utilisez, mais en général, l’idée est la même. Dans l'init du gestionnaire, nous devons créer les tableaux d'entrée, créer un lecteur et l'init.
classe publique Game public Character mPlayer; bool [] mInputs; bool [] mPrevInputs; void Start () input = new bool [(int) KeyInput.Count]; prevInputs = new bool [(int) KeyInput.Count]; player.CharacterInit (entrées, prevInputs);
De plus, dans la mise à jour du manager, nous devons mettre à jour le joueur et les entrées du joueur..
void Update () input [(int) KeyInput.GoRight] = Input.GetKey (goRightKey); input [(int) KeyInput.GoLeft] = Input.GetKey (goLeftKey); input [(int) KeyInput.GoDown] = Input.GetKey (goDownKey); input [(int) KeyInput.Jump] = Input.GetKey (goJumpKey); void FixedUpdate () player.CharacterUpdate ();
Notez que nous mettons à jour la physique du personnage dans la mise à jour corrigée. Cela garantira que les sauts auront toujours la même hauteur, quelle que soit la fréquence de trame avec laquelle notre jeu fonctionne. Glenn Fiedler a publié un excellent article sur la façon de corriger le pas de temps, au cas où vous n'utiliseriez pas Unity..
À ce stade, nous pouvons tester le mouvement du personnage et voir comment il se sent. Si nous n’aimons pas cela, nous pouvons toujours changer les paramètres ou la façon dont la vitesse change en appuyant sur les touches.
Les contrôles de personnage peuvent sembler très légers et pas aussi agréables qu'un mouvement basé sur la quantité de mouvement, mais tout dépend du type de contrôle qui conviendra le mieux à votre jeu. Heureusement, il est assez facile de changer le comportement du personnage. il suffit de modifier la manière dont la valeur de vitesse change dans les états de marche et de saut.
Voilà pour la première partie de la série. Nous nous sommes retrouvés avec un schéma de mouvement de personnage simple, mais pas beaucoup plus. Le plus important est que nous ayons tracé la voie pour la prochaine partie, dans laquelle nous allons faire en sorte que le personnage interagisse avec un tilemap.