Les comportements de direction sont parfaits pour créer des modèles de mouvement réalistes, mais ils le sont encore plus si vous pouvez les contrôler, les utiliser et les combiner facilement. Dans ce tutoriel, je discuterai et couvrirai la mise en œuvre d'un gestionnaire de mouvement pour tous nos comportements précédemment discutés..
Remarque: Bien que ce tutoriel ait été écrit avec AS3 et Flash, vous devriez pouvoir utiliser les mêmes techniques et concepts dans presque tous les environnements de développement de jeux. Vous devez avoir une compréhension de base des vecteurs mathématiques.
Comme indiqué précédemment, chaque comportement de direction produit une force résultante (appelée "force de direction") qui est ajoutée au vecteur vitesse. La direction et la magnitude de cette force guideront le personnage, le faisant bouger selon un schéma (chercher, fuir, errer, etc.). Le calcul général est:
pilotage = chercher (); // cela peut être n'importe quel comportement direction = tronqué (direction, max_force) direction = direction / vitesse de la masse = tronqué (vitesse + direction, vitesse maximale) position = position + vitesse
La force de direction étant un vecteur, elle peut être ajoutée à n’importe quel autre vecteur (tout comme la vitesse). Cependant, la vraie "magie" réside dans le fait que vous pouvez ajouter plusieurs forces de direction ensemble - c'est aussi simple que:
direction = rien (); // le vecteur nul, signifiant "magnitude zéro force" Steering = Steering + seek (); direction = direction + fuite (); (…) Direction = tronqué (direction, force_max) direction = direction / vitesse de masse = tronqué (vitesse + direction, vitesse_max) position = position + vitesse
Les forces de direction combinées produiront un vecteur qui représente tout ces forces. Dans l’extrait de code ci-dessus, la force de direction qui en résulte incitera le personnage à rechercher en même temps il va fuir quelque chose autre.
Vous trouverez ci-dessous quelques exemples de forces de direction combinées pour produire une seule force de direction:
La combinaison des forces de direction produira des schémas de mouvement extrêmement complexes sans effort. Imaginez à quel point il serait difficile d'écrire du code pour qu'un personnage cherche quelque chose, tout en évitant une zone spécifique, sans utiliser de vecteurs ni de forces?
Cela nécessiterait le calcul de distances, de surfaces, de chemins, de graphiques, etc. Si les choses bougent, tous ces calculs doivent être répétés de temps en temps, car l'environnement change constamment.
Avec les comportements de direction, toutes les forces sont dynamique. Ils sont censés être calculés à chaque mise à jour du jeu afin qu'ils réagissent naturellement et de manière transparente aux changements de l'environnement..
La démo ci-dessous montre les navires qui vont chercher le curseur de la souris mais fuiront le centre de l'écran, les deux en même temps:
Pour pouvoir utiliser plusieurs comportements en même temps de manière simple et facile, un gestionnaire de mouvement est très pratique. L'idée est de créer une "boîte noire" pouvant être connectée à n'importe quelle entité existante, ce qui la rend capable d'exécuter ces comportements..
Le gestionnaire a une référence à l'entité à laquelle il est branché ("l'hôte"). Le gestionnaire fournira à l'hôte un ensemble de méthodes, telles que chercher()
et fuir()
. Chaque fois que de telles méthodes sont appelées, le gestionnaire met à jour ses propriétés internes pour générer un vecteur de force de direction..
Une fois que tous les appels ont été traités, le gestionnaire ajoute la force de guidage résultante au vecteur de vitesse de l'hôte. Cela modifiera la magnitude et la direction du vecteur vitesse de l'hôte en fonction des comportements actifs..
La figure ci-dessous illustre l'architecture:
Le gestionnaire dispose d'un ensemble de méthodes, chacune représentant un comportement distinct. Chaque comportement doit être fourni avec différentes informations externes pour pouvoir fonctionner.
Le comportement de recherche, par exemple, nécessite un point dans l’espace utilisé pour calculer la force de direction vers cet endroit; poursuite a besoin de plusieurs informations provenant de la cible, telles que la position actuelle et la vitesse. Un point dans l’espace peut être exprimé comme une instance de Point
ou Vector2D
, les deux classes assez communes dans n'importe quel cadre.
La cible utilisée dans le comportement de poursuite, cependant, peut être n'importe quoi. Afin de rendre le gestionnaire de mouvement assez générique, il doit recevoir une cible qui, indépendamment de son type, est en mesure de répondre à quelques "questions", telles que "Quelle est votre vitesse actuelle?". En utilisant certains principes de la programmation orientée objet, il peut être réalisé avec interfaces.
En supposant l'interface IBoid
décrit une entité pouvant être gérée par le responsable du mouvement, toute classe du jeu peut utiliser des comportements de direction, à condition qu'elle implémente IBoid
. Cette interface a la structure suivante:
interface publique IBoid function getVelocity (): Vector3D; fonction getMaxVelocity (): nombre; fonction getPosition (): Vector3D; fonction getMass (): nombre;
Maintenant que le manager peut interagir avec toutes les entités du jeu de manière générique, sa structure de base peut être créée. Le gestionnaire est composé de deux propriétés (la force de pilotage résultante et la référence de l'hôte) et d'un ensemble de méthodes publiques, une pour chaque comportement:
classe publique SteeringManager direction publique var: Vector3D; public var host: IBoid; // Le constructeur fonction publique SteeringManager (hôte: IBoid) this.host = hôte; this.steering = nouveau Vector3D (0, 0); // L'API publique (une méthode pour chaque comportement) fonction publique recherche (cible: Vector3D, slowingRadius: Nombre = 20): void fonction publique fuir (cible: Vector3D): void fonction publique wander (): void fonction publique evade (cible: IBoid): void poursuite de la fonction publique (cible: IBoid): void // Méthode de mise à jour. // Doit être appelé après que tous les comportements ont été appelés. Public function update (): void // Réinitialise la force de pilotage interne. fonction publique reset (): void // Fonction privée de l'API interne doSeek (cible: Vector3D, slowingRadius: Number = 0): Vector3D fonction privée doFlee (cible: Vector3D): Vector3D fonction privée doWander (): Vector3D fonction privée doEvade (cible: IBoid): Vector3D fonction privée doPursuit (cible: IBoid): Vector3D
Lorsque le gestionnaire est instancié, il doit recevoir une référence à l'hôte auquel il est connecté. Cela permettra au gestionnaire de changer le vecteur vitesse hôte en fonction des comportements actifs.
Chaque comportement est représenté par deux méthodes, une publique et une privée. En utilisant chercher comme exemple:
fonction publique recherche (cible: Vector3D, slowingRadius: Number = 20): void fonction privée doSeek (cible: Vector3D, slowingRadius: Number = 0): Vector3D
Le public chercher()
sera invoqué pour dire au gestionnaire d’appliquer ce comportement spécifique. La méthode n'a pas de valeur de retour et ses paramètres sont liés au comportement lui-même, tel qu'un point dans l'espace. Sous le capot la méthode privée doSeek ()
sera invoqué et sa valeur de retour, la force de pilotage calculée pour ce comportement spécifique, sera ajoutée à la pilotage
propriété.
Le code suivant illustre l'implémentation de seek:
// La méthode de publication. // Reçoit une cible à rechercher et un slowingRadius (utilisé pour effectuer arrive). fonction publique recherche (cible: Vector3D, slowingRadius: Number = 20): void Steering.incrementBy (doSeek (cible, slowingRadius)); // L'implémentation réelle de la fonction privée search (avec le code d'arrivée inclus) doSeek (cible: Vector3D, slowingRadius: Number = 0): Vector3D var force: Vector3D; var distance: Nombre; désiré = cible.sous-extraire (host.getPosition ()); distance = longueur désirée; désiré.normalise (); si (distance <= slowingRadius) desired.scaleBy(host.getMaxVelocity() * distance/slowingRadius); else desired.scaleBy(host.getMaxVelocity()); force = desired.subtract(host.getVelocity()); return force;
Toutes les autres méthodes de comportement sont implémentées de manière très similaire. le poursuite()
La méthode, par exemple, ressemblera à ceci:
fonction publique poursuite (cible: IBoid): void Steering.incrementBy (doPursuit (cible)); fonction privée doPursuit (cible: IBoid): Vector3D distance = target.getPosition (). subtract (host.getPosition ()); var updatesNeeded: Number = distance.length / host.getMaxVelocity (); var tv: Vector3D = target.getVelocity (). clone (); tv.scaleBy (updatesNeeded); targetFuturePosition = target.getPosition (). clone (). add (tv); return doSeek (targetFuturePosition);
En utilisant le code des tutoriels précédents, tout ce que vous avez à faire est de les adapter sous forme de comportement()
et doBehavior ()
, afin qu'ils puissent être ajoutés au gestionnaire de mouvement.
Chaque fois que la méthode d’un comportement est appelée, la force résultante qu’elle produit est ajoutée à la stratégie du gestionnaire. pilotage
propriété. En conséquence, la propriété accumulera toutes les forces de direction.
Lorsque tous les comportements ont été invoqués, le responsable doit appliquer la force de commande actuelle à la vélocité de l'hôte afin qu'elle se déplace en fonction des comportements actifs. Il est effectué dans le mettre à jour()
méthode du gestionnaire de mouvement:
fonction publique update (): void var velocity: Vector3D = host.getVelocity (); position var: Vector3D = host.getPosition (); tronquer (direction, MAX_FORCE); Steering.scaleBy (1 / host.getMass ()); vitesse.incrementBy (pilotage); truncate (velocity, host.getMaxVelocity ()); position.incrementBy (vélocité);
La méthode ci-dessus doit être invoquée par l'hôte (ou par toute autre entité de jeu) après l'appel de tous les comportements. Sinon, l'hôte ne modifiera jamais son vecteur de vélocité pour correspondre aux comportements actifs..
Supposons une classe nommée Proie
devrait se déplacer en utilisant le comportement de la direction, mais pour le moment il n’a pas de code de direction ni le gestionnaire de mouvements. Sa structure ressemblera à ceci:
classe publique Prey public var position: Vector3D; vitesse de propagation publique: Vector3D; public var masse: Number; fonction publique Prey (posX: nombre, posY: nombre, masse totale: nombre) position = nouveau Vector3D (posX, posY); vélocité = nouveau Vector3D (-1, -2); masse = masse totale; x = position.x; y = position.y; public function update (): void velocity.normalize (); velocity.scaleBy (MAX_VELOCITY); vélocité.échelleBy (1 / masse); truncate (vélocité, MAX_VELOCITY); position = position.add (vélocité); x = position.x; y = position.y;
À l'aide de cette structure, les instances de classe peuvent être déplacées à l'aide de l'intégration d'Euler, tout comme la toute première démonstration du didacticiel de recherche. Pour pouvoir utiliser le gestionnaire, il faut une propriété référençant le gestionnaire de mouvements et il doit implémenter le IBoid
interface:
classe publique Prey implémente IBoid public var position: Vector3D; vitesse de propagation publique: Vector3D; public var masse: Number; Public Var Steering: SteeringManager; fonction publique Prey (posX: nombre, posY: nombre, masse totale: nombre) position = nouveau Vector3D (posX, posY); vélocité = nouveau Vector3D (-1, -2); masse = masse totale; Steering = new SteeringManager (this); x = position.x; y = position.y; public function update (): void velocity.normalize (); velocity.scaleBy (MAX_VELOCITY); vélocité.échelleBy (1 / masse); truncate (vélocité, MAX_VELOCITY); position = position.add (vélocité); x = position.x; y = position.y; // Ci-dessous, les méthodes requises par l'interface IBoid. fonction publique getVelocity (): Vector3D return velocity; fonction publique getMaxVelocity (): Number return 3; fonction publique getPosition (): Vector3D position de retour; fonction publique getMass (): Number return mass;
le mettre à jour()
méthode doit être modifiée en conséquence pour que le gestionnaire puisse également être mis à jour:
public function update (): void // Amène la proie à errer… Steering.wander (); // Met à jour le gestionnaire pour qu'il modifie le vecteur vitesse des proies. // Le gestionnaire effectuera également l'intégration d'Euler en modifiant // le vecteur "position". pilotage.update (); // Une fois que le gestionnaire a mis à jour ses structures internes, il ne // suffit que de mettre à jour notre position en fonction du vecteur "position". x = position.x; y = position.y;
Tous les comportements peuvent être utilisés en même temps, à condition que tous les appels de méthode soient passés avant le gestionnaire. mettre à jour()
invocation, qui applique la force de guidage accumulée au vecteur vitesse de l'hôte.
Le code ci-dessous illustre une autre version de Prey. mettre à jour()
méthode, mais cette fois, il cherchera une position sur la carte et échappera à un autre personnage (les deux en même temps):
fonction publique update (): void destination var: Vector3D = getDestination (); // l'endroit où chercher var hunter: IBoid = getHunter (); // récupère l'entité qui nous chasse // Cherche la destination et évite le chasseur (en même temps!) Steering.seek (destination); Steering.evade (chasseur); // Met à jour le gestionnaire pour qu'il modifie le vecteur vitesse des proies. // Le gestionnaire effectuera également l'intégration d'Euler en modifiant // le vecteur "position". pilotage.update (); // Une fois que le gestionnaire a mis à jour ses structures internes, tout ce que nous devons // faire est de mettre à jour notre position en fonction du vecteur "position". x = position.x; y = position.y;
La démo ci-dessous illustre un schéma de mouvement complexe dans lequel plusieurs comportements sont combinés. Il y a deux types de personnages dans la scène: le chasseur et le Proie.
Le chasseur poursuivre une proie si elle se rapproche suffisamment; il poursuivra aussi longtemps que durera l'endurance; quand il manque d’endurance, la poursuite est interrompue et le chasseur errer jusqu'à ce qu'il récupère ses niveaux d'endurance.
Voici le chasseur mettre à jour()
méthode:
public function update (): void if (reposant && stamina ++> = MAX_STAMINA) resting = false; if (proie! = null &&! au repos) Steering.Pursuit (proie); endurance - = 2; si (endurance <= 0) prey = null; resting = true; else steering.wander(); prey = getClosestPrey(position); steering.update(); x = position.x; y = position.y;
La proie sera errer indéfiniment. Si le chasseur s'approche trop, il le fera éluder. Si le curseur de la souris est proche et qu’il n’ya pas de chasseur, la proie chercher le curseur de la souris.
Voici le Prey mettre à jour()
méthode:
public function update (): void var distance: Number = Vector3D.distance (position, Game.mouse); hunter = getHunterWithinRange (position); if (chasseur! = null) Steering.evade (chasseur); si (distance <= 300 && hunter == null) steering.seek(Game.mouse, 30); else if(hunter == null) steering.wander(); steering.update(); x = position.x; y = position.y;
Le résultat final (le gris est errant, le vert est recherché, l’orange est poursuivi, le rouge est évité):
Un gestionnaire de mouvement est très utile pour contrôler plusieurs comportements de direction en même temps. La combinaison de tels comportements peut produire des schémas de mouvements très complexes, permettant à une entité de jeu de rechercher une chose en même temps qu'elle en évite une autre.
J'espère que vous avez aimé le système de gestion décrit et mis en œuvre dans ce tutoriel et que vous l'utilisez dans vos jeux. Merci pour la lecture! N'oubliez pas de vous tenir au courant en nous suivant sur Twitter, Facebook ou Google+.