Dans cette série de didacticiels, je vais vous montrer comment créer un jeu de tir au néon à deux bâtons comme Geometry Wars, que nous appellerons Shape Blaster, dans XNA. Le but de ces tutoriels n'est pas de vous laisser avec une réplique exacte de Geometry Wars, mais plutôt de passer en revue les éléments nécessaires qui vous permettront de créer votre propre variante de haute qualité..
Je vous encourage à développer et à expérimenter avec le code donné dans ces tutoriels. Nous allons couvrir ces sujets à travers la série:
Voici ce que nous aurons à la fin de la série:
Avertissement: fort!Et voici ce que nous aurons à la fin de cette première partie:
Avertissement: fort!La musique et les effets sonores que vous pouvez entendre dans ces vidéos ont été créés par RetroModular, et vous pouvez en savoir plus sur la façon dont il l'a fait à Audiotuts.+.
Les sprites sont de Jacob Zinman-Jeanes, notre concepteur résident Tuts +. Toutes les illustrations se trouvent dans le fichier source zip de téléchargement.
La police est Nova Square, de Wojciech Kalinowski.Commençons.
Dans ce tutoriel, nous allons créer un jeu de tir double-stick; le joueur contrôlera le vaisseau avec le clavier, le clavier et la souris, ou les deux manettes d'un gamepad.
Nous utilisons plusieurs classes pour accomplir ceci:
Entité
: La classe de base pour les ennemis, les balles et le vaisseau du joueur. Les entités peuvent se déplacer et être dessinées.Balle
et PlayerShip
.EntityManager
: Garde la trace de toutes les entités du jeu et effectue la détection de collision.Contribution
: Aide à gérer les entrées du clavier, de la souris et du gamepad.Art
: Charge et conserve des références aux textures nécessaires au jeu.Du son
: Charge et conserve des références aux sons et à la musique.MathUtil
et Les extensions
: Contient des méthodes statiques et des méthodes d'extension utiles.GameRoot
: Contrôle la boucle principale du jeu. C'est le Game1
la classe XNA génère automatiquement, renommé.Le code de ce tutoriel se veut simple et facile à comprendre. Il n’aura pas toutes les fonctionnalités ou une architecture compliquée conçue pour prendre en charge tous les besoins possibles. Au contraire, il ne fera que ce qu'il doit faire. En gardant les choses simples, il vous sera plus facile de comprendre les concepts, puis de les modifier et de les développer dans votre propre jeu unique..
Créez un nouveau projet XNA. Renommer le Game1
classe à quelque chose de plus approprié. Je l'ai appelé GameRoot
.
Commençons par créer une classe de base pour nos entités de jeu..
classe abstraite Entity image protégée Texture2D; // La teinte de l'image. Cela nous permettra également de changer la transparence. protégé Couleur couleur = Couleur.Bleu; public Vector2 Position, Velocity; float Orientation; flotteur public Rayon = 20; // utilisé pour la détection de collision circulaire public bool IsExpired; // true si l'entité a été détruite et doit être supprimée. public Vector2 Size get return image == null? Vector2.Zero: nouveau Vector2 (image.Width, image.Height); public abstract void Update (); public virtual void Draw (spriteBatch spriteBatch) spriteBatch.Draw (image, Position, null, couleur, Orientation, Taille / 2f, 1f, 0, 0);
Toutes nos entités (ennemis, balles et le vaisseau du joueur) ont des propriétés de base telles qu'une image et une position. Est expiré
sera utilisé pour indiquer que l'entité a été détruite et devrait être supprimé de toute liste contenant une référence à celle-ci.
Ensuite, nous créons un EntityManager
suivre nos entités et les mettre à jour et les dessiner.
Classe statique EntityManager Liste statiqueentités = nouvelle liste (); statique bool isUpdating; Liste statique addedEntities = nouvelle liste (); public static int Count get entités_de_retour; nombre; public static void Ajouter (entité entité) if (! isUpdating) Entités.Add (entité); else addedEntities.Add (entité); public static void Update () isUpdating = true; foreach (entité variable dans les entités) entity.Update (); isUpdating = false; foreach (entité var dans les entités ajoutées): ajouter (entité); addedEntities.Clear (); // supprime les entités expirées. entités = entités.Où (x =>! x.IsExpired) .ToList (); public static void Draw (SpriteBatch spriteBatch) foreach (entité var dans des entités) entity.Draw (spriteBatch);
Rappelez-vous que si vous modifiez une liste en effectuant une itération dessus, vous obtiendrez une exception. Le code ci-dessus s’occupe de cela en mettant en file d'attente toutes les entités ajoutées lors de la mise à jour dans une liste séparée et en les ajoutant une fois la mise à jour terminée.
Nous aurons besoin de charger des textures si nous voulons dessiner quelque chose. Nous allons faire une classe statique pour contenir des références à toutes nos textures.
classe statique Art public static Texture2D Player get; ensemble privé; public statique Texture2D Seeker get; ensemble privé; public statique Texture2D Wanderer get; ensemble privé; public statique Texture2D Bullet get; ensemble privé; public statique Texture2D Pointer get; ensemble privé; public static void Load (contenu ContentManager) Player = content.Load("Joueur"); Seeker = content.Load ("Chercheur"); Wanderer = content.Load ("Vagabond"); Bullet = content.Load ("Balle"); Pointeur = content.Load ("Aiguille");
Charger l'art en appelant Art.Load (Contenu)
dans GameRoot.LoadContent ()
. De plus, un certain nombre de classes auront besoin de connaître les dimensions de l’écran, ajoutez donc les propriétés suivantes à GameRoot
:
public statique Instance GameRoot get; ensemble privé; public statique Viewport Viewport get return Instance.GraphicsDevice.Viewport; public static Vector2 ScreenSize get return new Vector2 (Viewport.Width, Viewport.Height);
Et dans le GameRoot
constructeur, ajouter:
Instance = this;
Maintenant, nous allons commencer à écrire le PlayerShip
classe.
class PlayerShip: Entity instance privée privée PlayerShip; Instance publique publique PlayerShip get if (instance == null) instance = new PlayerShip (); instance de retour; private PlayerShip () image = Art.Player; Position = GameRoot.ScreenSize / 2; Rayon = 10; public override void Update () // la logique du navire passe ici
Nous avons fait PlayerShip
un singleton, définissez son image et placez-la au centre de l'écran.
Enfin, ajoutons le vaisseau du joueur à la EntityManager
et mettre à jour et dessiner. Ajoutez le code suivant dans GameRoot
:
// in Initialize (), après l'appel à base.Initialize () EntityManager.Add (PlayerShip.Instance); // dans Update () EntityManager.Update (); // in Draw () GraphicsDevice.Clear (Color.Black); spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); EntityManager.Draw (spriteBatch); spriteBatch.End ();
Nous dessinons les sprites avec mélange additif, ce qui fait partie de ce qui leur donnera leur look néon. Si vous lancez le jeu à ce stade, vous devriez voir votre vaisseau au centre de l'écran. Cependant, il ne répond pas encore à l'entrée. Corrigeons ça.
Pour le mouvement, le joueur peut utiliser WASD sur le clavier ou la manette gauche sur une manette de jeu. Pour viser, ils peuvent utiliser les touches fléchées, la manette droite ou la souris. Nous n’obligerons pas le joueur à tenir le bouton de la souris pour tirer car il est inconfortable de maintenir le bouton continuellement. Cela nous laisse avec un petit problème: comment savoir si le joueur vise avec la souris, le clavier ou la manette de jeu?
Nous utiliserons le système suivant: nous ajouterons les entrées au clavier et à la manette de jeu. Si le joueur déplace la souris, nous passons au pointage de la souris. Si le joueur appuie sur les touches fléchées ou utilise la manette droite, nous désactivons la souris.
Une chose à noter: pousser un pouce en avant fera revenir un positif y valeur. En coordonnées d'écran, les valeurs y augmentent vers le bas. Nous voulons inverser l’axe des ordonnées sur le contrôleur pour que le fait de pousser la manette vers le haut nous oriente ou nous dirige vers le haut de l’écran..
Nous allons créer une classe statique pour garder une trace des différents périphériques d’entrée et prendre en charge la commutation entre les différents types de visée..
classe statique Input privé statique KeyboardState keyboardState, lastKeyboardState; statique privée MouseState mouseState, lastMouseState; statique statique privée GamePadState gamepadState, lastGamepadState; private statique bool isAimingWithMouse = false; public statique Vector2 MousePosition get return new Vector2 (mouseState.X, mouseState.Y); public static void Update () lastKeyboardState = keyboardState; lastMouseState = mouseState; lastGamepadState = gamepadState; keyboardState = Keyboard.GetState (); mouseState = Mouse.GetState (); gamepadState = GamePad.GetState (PlayerIndex.One); // Si le joueur a appuyé sur l'une des touches de direction ou utilise une manette de jeu pour viser, nous voulons désactiver la visée de la souris. Sinon, // si le joueur déplace la souris, activez le pointage de la souris. if (new [] Keys.Left, Keys.Right, Keys.Up, Keys.Down .Any (x => keyboardState.IsKeyDown (x)) || gamepadState.ThumbSticks.Right! = Vector2.Zero) isAimingWithMouse = faux; else if (MousePosition! = new Vector2 (lastMouseState.X, lastMouseState.Y)) isAimingWithMouse = true; // Vérifie si une touche vient juste d'être enfoncée. Public static bool WasKeyPressed (clé de clé) return lastKeyboardState.IsKeyUp (clé) && keyboardState.IsKeyDown (clé); public statique bool WasButtonPressed (bouton Boutons) return lastGamepadState.IsButtonUp (bouton) && gamepadState.IsButtonDown (bouton); public statique Vector2 GetMovementDirection () Vector2 direction = gamepadState.ThumbSticks.Left; direction.Y * = -1; // inverse l'axe des y si (keyboardState.IsKeyDown (Keys.A)) direction.X - = 1; if (keyboardState.IsKeyDown (Keys.D)) direction.X + = 1; if (keyboardState.IsKeyDown (Keys.W)) direction.Y - = 1; if (keyboardState.IsKeyDown (Keys.S)) direction.Y + = 1; // Fixe la longueur du vecteur au maximum à 1. if (direction.LengthSquared ()> 1) direction.Normalize (); direction de retour; public statique Vector2 GetAimDirection () if (isAimingWithMouse) return GetMouseAimDirection (); Vector2 direction = gamepadState.ThumbSticks.Right; direction.Y * = -1; if (keyboardState.IsKeyDown (Keys.Left)) direction.X - = 1; if (keyboardState.IsKeyDown (Keys.Right)) direction.X + = 1; if (keyboardState.IsKeyDown (Keys.Up)) direction.Y - = 1; if (keyboardState.IsKeyDown (Keys.Down)) direction.Y + = 1; // S'il n'y a pas d'entrée visée, retourne zéro. Sinon, normalisez la direction pour qu'elle ait une longueur de 1. if (direction == Vector2.Zero) renvoie Vector2.Zero; sinon, retourne Vector2.Normaliser (direction); private statique Vector2 GetMouseAimDirection () Vector2 direction = MousePosition - PlayerShip.Instance.Position; if (direction == Vector2.Zero) renvoie Vector2.Zero; sinon, retourne Vector2.Normaliser (direction); public statique bool WasBombButtonPressed () return WasButtonPressed (Buttons.LeftTrigger) || WasButtonPressed (Buttons.RightTrigger) || WasKeyPressed (Keys.Space);
Appel Input.Update ()
au début de GameRoot.Update ()
pour que la classe d'entrée fonctionne.
Pointe: Vous remarquerez peut-être que j'ai inclus une méthode pour les bombes. Nous n'utiliserons pas de bombes maintenant, mais cette méthode est là pour une utilisation future.
Vous pouvez également remarquer dans GetMovementDirection ()
J'ai écrit direction.LengthSquared ()> 1
. En utilisant LengthSquared ()
est une petite optimisation de la performance; le calcul du carré de la longueur est un peu plus rapide que le calcul de la longueur elle-même, car il évite l'opération de la racine carrée relativement lente. Vous verrez le code utiliser les carrés de longueurs ou de distances tout au long du programme. Dans ce cas particulier, la différence de performance est négligeable, mais cette optimisation peut faire la différence lorsqu'elle est utilisée dans des boucles serrées..
Nous sommes maintenant prêts à faire avancer le navire. Ajoutez ce code au PlayerShip.Update ()
méthode:
vitesse de flottement constante = 8; Velocity = speed * Input.GetMovementDirection (); Position + = vitesse; Position = Vector2.Clamp (Position, Size / 2, GameRoot.ScreenSize - Size / 2); if (Velocity.LengthSquared ()> 0) Orientation = Velocity.ToAngle ();
Cela fera bouger le navire à une vitesse maximale de huit pixels par image, fixera sa position de manière à ce qu'il ne puisse pas sortir de l'écran et fera pivoter le navire pour le faire face à la direction dans laquelle il se déplace..
ToAngle ()
est une méthode d'extension simple définie dans notre Les extensions
classe comme si:
public statique float ToAngle (ce vecteur Vector2) return (float) Math.Atan2 (vector.Y, vector.X);
Si vous lancez le jeu maintenant, vous devriez pouvoir piloter le vaisseau. Maintenant faisons-le tirer.
Tout d'abord, nous avons besoin d'une classe pour les balles.
class Bullet: Entity Bullet public (position Vector2, vitesse Vector2) image = Art.Bullet; Position = position; Vélocité = vélocité; Orientation = Velocity.ToAngle (); Rayon = 8; public override void Update () if (Velocity.LengthSquared ()> 0) Orientation = Velocity.ToAngle (); Position + = vitesse; // supprime les puces qui sortent de l'écran si (! GameRoot.Viewport.Bounds.Contains (Position.ToPoint ())) IsExpired = true;
Nous voulons un bref délai de récupération entre les puces, alors ajoutez les champs suivants au PlayerShip
classe.
const int cooldownFrames = 6; int cooldownRemaining = 0; Random statique statique = new Random ();
Ajoutez également le code suivant à PlayerShip.Update ()
.
var aim = Input.GetAimDirection (); if (aim.LengthSquared ()> 0 && cooldownRemaining <= 0) cooldownRemaining = cooldownFrames; float aimAngle = aim.ToAngle(); Quaternion aimQuat = Quaternion.CreateFromYawPitchRoll(0, 0, aimAngle); float randomSpread = rand.NextFloat(-0.04f, 0.04f) + rand.NextFloat(-0.04f, 0.04f); Vector2 vel = MathUtil.FromPolar(aimAngle + randomSpread, 11f); Vector2 offset = Vector2.Transform(new Vector2(25, -8), aimQuat); EntityManager.Add(new Bullet(Position + offset, vel)); offset = Vector2.Transform(new Vector2(25, 8), aimQuat); EntityManager.Add(new Bullet(Position + offset, vel)); if (cooldownRemaining > 0) cooldownRemaining--;
Ce code crée deux puces qui voyagent en parallèle. Cela ajoute une petite quantité de hasard à la direction. Cela fait que les tirs se déploient un peu comme une mitrailleuse. Nous additionnons deux nombres aléatoires, ce qui rend leur somme plus susceptible d'être centrée (autour de zéro) et moins susceptible d'envoyer des puces au loin. Nous utilisons un quaternion pour faire pivoter la position initiale des balles dans la direction dans laquelle elles se déplacent.
Nous avons également utilisé deux nouvelles méthodes d'assistance:
Random.NextFloat ()
retourne un float entre une valeur minimum et maximum.MathUtil.FromPolar ()
crée un Vecteur2
d'un angle et d'une magnitude.// in Extensions public static float NextFloat (ce Random rand, float minValue, float maxValue) return (float) rand.NextDouble () * (maxValue - minValue) + minValue; // in MathUtil public static Vector2 FromPolar (angle de flottement, magnitude du flottant) magnitude de retour * nouveau Vector2 ((float) Math.Cos (angle), (float) Math.Sin (angle));
Il y a encore une chose que nous devrions faire maintenant que nous avons le Contribution
classe. Dessinez un curseur de souris personnalisé pour mieux voir où le navire vise. Dans GameRoot.Draw
, dessine simplement Art.Pointer
à la position de la souris.
spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); EntityManager.Draw (spriteBatch); // trace le curseur de la souris personnalisé spriteBatch.Draw (Art.Pointer, Input.MousePosition, Color.White); spriteBatch.End ();
Si vous testez le jeu maintenant, vous pourrez déplacer le vaisseau avec les touches WASD ou la manette de pouce gauche, et viser le flot continu de balles avec les flèches, la souris ou la manette de droite..
Dans la prochaine partie, nous terminerons le jeu en ajoutant des ennemis et un score.