Dans cette série de didacticiels, je vais vous montrer comment créer un jeu de tir au néon, tel que Geometry Wars, 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é..
Dans cette partie, nous allons développer le didacticiel précédent en ajoutant des ennemis, en détectant les collisions et en marquant des points..
Voici ce que nous aurons à la fin:
Avertissement: fort!Nous allons ajouter les nouvelles classes suivantes pour gérer cela:
Ennemi
EnemySpawner
: Responsable de la création d'ennemis et de l'augmentation progressive de la difficulté du jeu.PlayerStatus
: Suit le score du joueur, le meilleur score et la vie.Vous avez peut-être remarqué qu'il y a deux types d'ennemis dans la vidéo, mais il n'y en a qu'un Ennemi
classe. Nous pourrions dériver des sous-classes de Ennemi
pour chaque type d'ennemi. Cependant, je préfère éviter les hiérarchies de classes profondes car elles présentent des inconvénients:
Mammifère
et Oiseau
, qui proviennent tous deux de Animal
. le Oiseau
la classe a Voler()
méthode. Ensuite, vous décidez d’ajouter un Chauve souris
classe qui dérive de Mammifère
et peut aussi voler. Pour partager cette fonctionnalité en utilisant uniquement l'héritage, vous devez déplacer le Voler()
méthode à la Animal
classe où il n'appartient pas. De plus, vous ne pouvez pas supprimer les méthodes des classes dérivées, donc si vous faites un manchot
classe qui dérive de Oiseau
, il aurait aussi eu un Voler()
méthode.Pour ce tutoriel, nous allons privilégier la composition au détriment de l'héritage pour implémenter les différents types d'ennemis. Nous y parviendrons en créant divers comportements réutilisables que nous pourrons ajouter aux ennemis. Nous pouvons alors facilement mélanger et assortir les comportements lorsque nous créons de nouveaux types d'ennemis. Par exemple, si nous avions déjà un FollowPlayer
comportement et un DodgeBullet
comportement, nous pourrions faire un nouvel ennemi qui fait les deux simplement en ajoutant les deux comportements.
Les ennemis auront quelques propriétés supplémentaires sur les entités. Afin de donner au joueur le temps de réagir, nous ferons en sorte que les ennemis disparaissent progressivement avant de devenir actifs et dangereux..
Codons la structure de base du Ennemi
classe.
classe Enemy: Entity private int timeUntilStart = 60; public bool IsActive get return timeUntilStart <= 0; public Enemy(Texture2D image, Vector2 position) this.image = image; Position = position; Radius = image.Width / 2f; color = Color.Transparent; public override void Update() if (timeUntilStart <= 0) // enemy behaviour logic goes here. else timeUntilStart--; color = Color.White * (1 - timeUntilStart / 60f); Position += Velocity; Position = Vector2.Clamp(Position, Size / 2, GameRoot.ScreenSize - Size / 2); Velocity *= 0.8f; public void WasShot() IsExpired = true;
Ce code fera fondre les ennemis pendant 60 images et permettra à leur vitesse de fonctionner. Multiplier la vitesse par 0,8 simule un effet de friction. Si nous faisons en sorte que les ennemis accélèrent à un rythme constant, ce frottement leur permettra de s’approcher en douceur d’une vitesse maximale. J'aime la simplicité et la douceur de ce type de frottement, mais vous pouvez utiliser une formule différente selon l'effet souhaité..
le A été abattu()
La méthode sera appelée lorsque l’ennemi se fera tirer dessus. Nous en ajouterons plus tard dans la série.
Nous voulons que différents types d’ennemis se comportent différemment. Nous accomplirons cela en assignant des comportements. Un comportement utilisera une fonction personnalisée qui exécute chaque image pour contrôler l'ennemi. Nous allons mettre en œuvre le comportement en utilisant un itérateur.
Les itérateurs (également appelés générateurs) en C # sont des méthodes spéciales qui peuvent s’arrêter à mi-parcours et reprendre ensuite là où ils s’étaient arrêtés. Vous pouvez créer un itérateur en faisant une méthode avec un type de retour de IEnumerable <>
et en utilisant le mot-clé de rendement où vous voulez qu'il retourne et reprenne plus tard. Les itérateurs en C # exigent que vous rendiez quelque chose lorsque vous cédiez. Nous n'avons pas vraiment besoin de retourner quoi que ce soit, nos itérateurs donneront simplement zéro.
Notre comportement le plus simple sera le FollowPlayer ()
comportement montré ci-dessous.
IEnumerableFollowPlayer (float accéleration = 1f) while (true) Velocity + = (PlayerShip.Instance.Position - Position) .ScaleTo (accélération); if (Velocity! = Vector2.Zero) Orientation = Velocity.ToAngle (); rendement retour 0;
Cela fait simplement que l'ennemi accélère vers le joueur à un rythme constant. Le frottement que nous avons ajouté précédemment assurera qu’il atteindra éventuellement une vitesse maximale (5 pixels par image lorsque l’accélération est égale à 1 puisque \ (0.8 \ times 5 + 1 = 5 \)). Chaque image, cette méthode fonctionnera jusqu’à atteindre la déclaration de rendement, puis reprendra là où elle s’est arrêtée..
Vous vous demandez peut-être pourquoi nous nous ennuyons avec les itérateurs, car nous aurions pu accomplir la même tâche plus facilement avec un simple délégué. L'utilisation d'itérateurs est payante avec des méthodes plus complexes qui nécessiteraient sinon de stocker l'état dans des variables membres de la classe.
Par exemple, voici un comportement qui fait bouger un ennemi en carré:
IEnumerableMoveInASquare () const int framesPerSide = 30; while (true) // se déplace vers la droite pour 30 images pour (int i = 0; i < framesPerSide; i++) Velocity = Vector2.UnitX; yield return 0; // move down for (int i = 0; i < framesPerSide; i++) Velocity = Vector2.UnitY; yield return 0; // move left for (int i = 0; i < framesPerSide; i++) Velocity = -Vector2.UnitX; yield return 0; // move up for (int i = 0; i < framesPerSide; i++) Velocity = -Vector2.UnitY; yield return 0;
Ce qui est bien, c’est que non seulement nous enregistre certaines variables d’instance, mais il structure également le code de manière très logique. Vous pouvez voir tout de suite que l'ennemi se déplacera à droite, puis vers le bas, puis à gauche, puis vers le haut et se répète. Si vous deviez implémenter cette méthode comme une machine à états, le flux de contrôle serait moins évident.
Ajoutons l'échafaudage nécessaire pour que les comportements fonctionnent. Les ennemis doivent stocker leurs comportements, nous allons donc ajouter une variable à la Ennemi
classe.
liste privée> comportements = nouvelle liste > ();
Notez qu'un comportement a le type IEnumerator
, ne pas IEnumerable
. Vous pouvez penser à la IEnumerable
comme modèle pour le comportement et la IEnumerator
en tant qu'instance en cours d'exécution. le IEnumerator
se souvient où nous sommes dans le comportement et reprendra où il s'est arrêté lorsque vous appelez son MoveNext ()
méthode. Nous allons passer en revue tous les comportements de l’ennemi et les appeler MoveNext ()
sur chacun d'eux. Si MoveNext ()
renvoie false, cela signifie que le comportement est terminé, nous devons donc le supprimer de la liste..
Nous allons ajouter les méthodes suivantes à la Ennemi
classe:
privé void AddBehaviour (IEnumerablecomportement) behavior.Add (behavior.GetEnumerator ()); void privé ApplyBehaviours () pour (int i = 0; i < behaviours.Count; i++) if (!behaviours[i].MoveNext()) behaviours.RemoveAt(i--);
Et nous modifierons le Mettre à jour()
méthode pour appeler ApplyBehaviours ()
:
si (timeUntilStart <= 0) ApplyBehaviours(); //…
Nous pouvons maintenant créer une méthode statique pour créer des ennemis à la recherche. Tout ce que nous avons à faire est de choisir l’image que nous voulons et d’ajouter le FollowPlayer ()
comportement.
public Enemy CreateSeeker (position Vector2) var ennemis = nouvel ennemi (Art.Seeker, position); ennemi.AddBehaviour (ennemi.FollowPlayer ()); renvoyer l'ennemi;
Pour créer un ennemi qui se déplace de manière aléatoire, nous lui demanderons de choisir une direction, puis de faire de petits ajustements aléatoires dans cette direction. Cependant, si nous ajustons la direction à chaque image, le mouvement sera saccadé. Nous ne modifierons donc la direction que périodiquement. Si l'ennemi se trouve au bord de l'écran, nous lui demanderons de choisir une nouvelle direction aléatoire qui pointe vers le mur..
IEnumerableMoveRandomly () direction du flottant = rand.NextFloat (0, MathHelper.TwoPi); while (true) direction + = rand.NextFloat (-0.1f, 0.1f); direction = MathHelper.WrapAngle (direction); pour (int i = 0; i < 6; i++) Velocity += MathUtil.FromPolar(direction, 0.4f); Orientation -= 0.05f; var bounds = GameRoot.Viewport.Bounds; bounds.Inflate(-image.Width, -image.Height); // if the enemy is outside the bounds, make it move away from the edge if (!bounds.Contains(Position.ToPoint())) direction = (GameRoot.ScreenSize / 2 - Position).ToAngle() + rand.NextFloat(-MathHelper.PiOver2, MathHelper.PiOver2); yield return 0;
Nous pouvons maintenant créer une méthode d'usine pour créer des ennemis errants, un peu comme nous l'avions fait pour le chercheur:
public statique Enemy CreateWanderer (position Vector2) var ennemis = nouvel ennemi (Art.Wanderer, position); ennemi.AddBehaviour (ennemi.MoveRandomly ()); renvoyer l'ennemi;
Pour la détection de collision, nous modéliserons le vaisseau du joueur, les ennemis et les balles sous forme de cercles. La détection de collision circulaire est agréable car simple, rapide et ne change pas lorsque les objets pivotent. Si vous vous en souvenez, le Entité
La classe a un rayon et une position (la position fait référence au centre de l'entité). C'est tout ce dont nous avons besoin pour la détection de collision circulaire.
Le test de chaque entité par rapport à toutes les autres entités pouvant potentiellement entrer en collision peut être très lent si vous avez un grand nombre d'entités. Il existe de nombreuses techniques que vous pouvez utiliser pour accélérer la détection de collision de phase large, telles que les arbres à quatre arbres, les éléments de balayage et d'élagage et les arbres BSP. Cependant, pour l'instant, nous n'aurons que quelques douzaines d'entités à la fois, nous ne nous inquiétons donc pas de ces techniques plus complexes. Nous pouvons toujours les ajouter plus tard si nous en avons besoin.
Dans Shape Blaster, toutes les entités ne peuvent pas entrer en collision avec tous les autres types d’entités. Les balles et le vaisseau du joueur ne peuvent entrer en collision qu'avec des ennemis. Les ennemis peuvent également entrer en collision avec d'autres ennemis - cela les empêchera de se chevaucher.
Pour traiter ces différents types de collisions, nous allons ajouter deux nouvelles listes à la liste. EntityManager
garder une trace des balles et des ennemis. Chaque fois que nous ajoutons une entité à la EntityManager
, nous voudrons l'ajouter à la liste appropriée, nous allons donc faire un privé AddEntity ()
méthode pour le faire. Nous veillerons également à supprimer toutes les entités expirées de toutes les listes de chaque cadre..
Liste statiqueennemis = nouvelle liste (); Liste statique puces = nouvelle liste (); void statique privé AddEntity (entité entité) entités.Add (entité); if (entity is Bullet) balles.Add (entity as Bullet); sinon si (l'entité est ennemie) ennemis.Add (l'entité en tant qu'ennemi); //… // dans Update () bullets = bullets.Where (x =>! X.IsExpired) .ToList (); ennemis = ennemis.Où (x =>! x.IsExpired) .ToList ();
Remplacer les appels à entity.Add ()
dans EntityManager.Add ()
et EntityManager.Update ()
avec des appels à AddEntity ()
.
Ajoutons maintenant une méthode qui déterminera si deux entités entrent en collision:
private statique bool IsColliding (entité a, entité b) rayon de flottement = a.Radius + b.Radius; return! a.IsExpired &&! b.IsExpired && Vector2.DistanceSquared (a.Position, b.Position) < radius * radius;
Pour déterminer si deux cercles se chevauchent, il suffit de vérifier si la distance qui les sépare est inférieure à la somme de leurs rayons. Notre méthode optimise légèrement ceci en vérifiant si le carré de la distance est inférieur au carré de la somme des rayons. Rappelez-vous qu'il est un peu plus rapide de calculer la distance au carré que la distance réelle.
Différentes choses se produiront en fonction de la collision de deux objets. Si deux ennemis entrent en collision, nous voulons qu’ils se repoussent. Si une balle frappe un ennemi, la balle et l'ennemi doivent tous deux être détruits. Si le joueur touche un ennemi, le joueur doit mourir et le niveau doit être réinitialisé.
Nous allons ajouter un HandleCollision ()
méthode à la Ennemi
classe pour gérer les collisions entre ennemis:
public void HandleCollision (Autre ennemi) var d = Position - autre.Position; Vitesse + = 10 * d / (d.LengthSquared () + 1);
Cette méthode éloignera l'ennemi actuel de l'autre ennemi. Plus ils sont proches, plus il sera poussé, car la magnitude de (d / d.LengthSquared ())
est juste un sur la distance.
Ensuite, nous avons besoin d'une méthode pour gérer le vaisseau du joueur en train de se faire tuer. Lorsque cela se produit, le vaisseau du joueur disparaît pendant un court instant avant de réapparaître.
Nous commençons par ajouter deux nouveaux membres à PlayerShip
.
int framesUntilRespawn = 0; public bool IsDead get return framesUntilRespawn> 0;
Au tout début de PlayerShip.Update ()
, ajoutez ce qui suit:
if (IsDead) framesUntilRespawn--; revenir;
Et nous annulons Dessiner()
comme montré:
remplacer public void Draw (SpriteBatch spriteBatch) if (! IsDead) base.Draw (spriteBatch);
Enfin, nous ajoutons un Tuer()
méthode pour PlayerShip
.
vide public Kill () framesUntilRespawn = 60;
Maintenant que toutes les pièces sont en place, nous allons ajouter une méthode à la EntityManager
qui passe par toutes les entités et vérifie les collisions.
static void HandleCollisions () // gère les collisions entre ennemis pour (int i = 0; i < enemies.Count; i++) for (int j = i + 1; j < enemies.Count; j++) if (IsColliding(enemies[i], enemies[j])) enemies[i].HandleCollision(enemies[j]); enemies[j].HandleCollision(enemies[i]); // handle collisions between bullets and enemies for (int i = 0; i < enemies.Count; i++) for (int j = 0; j < bullets.Count; j++) if (IsColliding(enemies[i], bullets[j])) enemies[i].WasShot(); bullets[j].IsExpired = true; // handle collisions between the player and enemies for (int i = 0; i < enemies.Count; i++) if (enemies[i].IsActive && IsColliding(PlayerShip.Instance, enemies[i])) PlayerShip.Instance.Kill(); enemies.ForEach(x => x.WasShot ()); Pause;
Appelez cette méthode de Mettre à jour()
immédiatement après le réglage est à jour
à vrai
.
La dernière chose à faire est de faire la EnemySpawner
classe, qui est responsable de la création d'ennemis. Nous voulons que le jeu commence facilement et devienne plus difficile. EnemySpawner
créera des ennemis à un rythme croissant au fil du temps. Lorsque le joueur meurt, nous réinitialisons le EnemySpawner
à sa difficulté initiale.
classe statique EnemySpawner static Random rand = new Random (); float statique inverseSpawnChance = 60; public static void Update () if (! PlayerShip.Instance.IsDead && EntityManager.Count < 200) if (rand.Next((int)inverseSpawnChance) == 0) EntityManager.Add(Enemy.CreateSeeker(GetSpawnPosition())); if (rand.Next((int)inverseSpawnChance) == 0) EntityManager.Add(Enemy.CreateWanderer(GetSpawnPosition())); // slowly increase the spawn rate as time progresses if (inverseSpawnChance > 20) inverseSpawnChance - = 0,005f; privé statique Vector2 GetSpawnPosition () Vector2 pos; do pos = new Vector2 (rand.Next ((int) GameRoot.ScreenSize.X), rand.Next ((int) GameRoot.ScreenSize.Y)); while (Vector2.DistanceSquared (pos, PlayerShip.Instance.Position) < 250 * 250); return pos; public static void Reset() inverseSpawnChance = 60;
Chaque image, il y en a une dans inverseSpawnChance
de générer chaque type d'ennemi. Les chances de générer un ennemi augmentent progressivement jusqu'à atteindre un maximum de un sur vingt. Les ennemis sont toujours créés à au moins 250 pixels du joueur..
Faites attention à la boucle while GetSpawnPosition ()
. Cela fonctionnera efficacement tant que la zone dans laquelle les ennemis peuvent apparaître est plus grande que la zone où ils ne peuvent pas apparaître. Cependant, si vous agrandissez trop la zone interdite, vous obtiendrez une boucle infinie..
Appel EnemySpawner.Update ()
de GameRoot.Update ()
et appeler EnemySpawner.Reset ()
quand le joueur est tué.
Dans Shape Blaster, vous commencerez par quatre vies et vous gagnerez une vie supplémentaire tous les 2000 points. Vous recevez des points pour la destruction d'ennemis, différents types d'ennemis valant différentes quantités de points. Chaque ennemi détruit augmente également votre multiplicateur de score de un. Si vous ne tuez aucun ennemi dans un court laps de temps, votre multiplicateur sera réinitialisé. Le nombre total de points reçus de chaque ennemi que vous détruisez est le nombre de points que l'ennemi vaut, multiplié par votre multiplicateur actuel. Si vous perdez toutes vos vies, le jeu est terminé et vous démarrez un nouveau jeu avec votre score réinitialisé à zéro.
Pour gérer tout cela, nous allons créer une classe statique appelée PlayerStatus
.
static class PlayerStatus // temps nécessaire, en secondes, à un multiplicateur pour expirer. private const float multiplierExpiryTime = 0.8f; private const int maxMultiplier = 20; public static int Lives get; ensemble privé; public static int Score get; ensemble privé; public static int Multiplier get; ensemble privé; privé statique float multiplierTimeLeft; // temps jusqu'à l'expiration du multiplicateur actuel private static int scoreForExtraLife; // score requis pour gagner une vie supplémentaire // constructeur statique static PlayerStatus () Reset (); public static void Reset () Score = 0; Multiplicateur = 1; Vies = 4; scoreForExtraLife = 2000; multiplierTimeLeft = 0; public static void Update () if (Multiplier> 1) // met à jour le temporisateur de multiplicateur if ((multiplierTimeLeft - = (float) GameRoot.GameTime.ElapsedGameTime.TotalSeconds) <= 0) multiplierTimeLeft = multiplierExpiryTime; ResetMultiplier(); public static void AddPoints(int basePoints) if (PlayerShip.Instance.IsDead) return; Score += basePoints * Multiplier; while (Score >= scoreForExtraLife) scoreForExtraLife + = 2000; Vies ++; public statique vide IncreaseMultiplier () if (PlayerShip.Instance.IsDead) return; multiplierTimeLeft = multiplierExpiryTime; si (multiplicateur < maxMultiplier) Multiplier++; public static void ResetMultiplier() Multiplier = 1; public static void RemoveLife() Lives--;
Appel PlayerStatus.Update ()
de GameRoot.Update ()
quand le jeu n'est pas en pause.
Ensuite, nous voulons afficher votre score, vos vies et votre multiplicateur à l'écran. Pour ce faire, nous devrons ajouter un SpriteFont
dans le Contenu
projet et une variable correspondante dans le Art
classe, que nous nommerons Police de caractère
. Charger la police dans Art.Load ()
comme nous l'avons fait avec les textures.
Modifier la fin de GameRoot.Draw ()
où le curseur est dessiné comme indiqué ci-dessous.
spriteBatch.Begin (0, BlendState.Additive); spriteBatch.DrawString (Art.Font, "Lives:" + PlayerStatus.Lives, nouveau Vector2 (5), Color.White); DrawRightAlignedString ("Score:" + PlayerStatus.Score, 5); DrawRightAlignedString ("Multiplier:" + PlayerStatus.Multiplier, 35); // trace le curseur de la souris personnalisé spriteBatch.Draw (Art.Pointer, Input.MousePosition, Color.White); spriteBatch.End ();
DrawRightAlignedString ()
est une méthode d'assistance pour dessiner un texte aligné à droite de l'écran. Ajoutez-le à GameRoot
en ajoutant le code ci-dessous.
void privé DrawRightAlignedString (chaîne de caractères text, float y) var textWidth = Art.Font.MeasureString (text) .X; spriteBatch.DrawString (Art.Font, texte, nouveau Vector2 (ScreenSize.X - textWidth - 5, y), Color.White);
Maintenant, vos vies, vos scores et vos multiplicateurs devraient s’afficher à l’écran. Cependant, nous devons encore modifier ces valeurs en réponse aux événements du jeu. Ajouter une propriété appelée PointValue
au Ennemi
classe.
public int PointValue get; ensemble privé;
Définissez la valeur en points pour différents ennemis sur quelque chose qui vous semble approprié. J'ai fait les ennemis errants valant un point, et les ennemis en quête valant deux points.
Ensuite, ajoutez les deux lignes suivantes à Enemy.WasShot ()
pour augmenter le score et le multiplicateur du joueur:
PlayerStatus.AddPoints (PointValue); PlayerStatus.IncreaseMultiplier ();
Appel PlayerStatus.RemoveLife ()
dans PlayerShip.Kill ()
. Si le joueur perd toute sa vie, appelez PlayerStatus.Reset ()
réinitialiser leur score et leur vie au début d'un nouveau jeu.
Ajoutons la possibilité pour le jeu de suivre votre meilleur score. Nous voulons que ce score persiste d'une partie à l'autre, nous allons donc l'enregistrer dans un fichier. Nous allons rester très simples et enregistrer le meilleur score sous forme de numéro en clair dans un fichier du répertoire de travail actuel (ce sera le même répertoire que celui du jeu). .EXE
fichier).
Ajoutez les méthodes suivantes à PlayerStatus
:
private const string highScoreFilename = "highscore.txt"; private static int LoadHighScore () // renvoie si possible le meilleur score enregistré et renvoie 0 sinon score total; return File.Exists (highScoreFilename) && int.TryParse (File.ReadAllText (highScoreFilename), score final)? score: 0; void statique privé SaveHighScore (int score) File.WriteAllText (highScoreFilename, score.ToString ());
le LoadHighScore ()
méthode vérifie d'abord que le fichier de score élevé existe, puis qu'il contient un entier valide. La deuxième vérification n'échouera probablement jamais, à moins que l'utilisateur ne modifie manuellement le fichier du score le plus élevé en un élément invalide, mais il convient de rester prudent.
Nous voulons charger le meilleur score au début de la partie et le sauvegarder lorsque le joueur obtient un nouveau meilleur score. Nous allons modifier le constructeur statique et Réinitialiser()
méthodes en PlayerStatus
faire cela. Nous allons également ajouter une propriété d'assistance, IsGameOver
que nous utiliserons dans un instant.
public bool IsGameOver get return Lives == 0; static PlayerStatus () HighScore = LoadHighScore (); Réinitialiser(); public static void Reset () if (Score> HighScore) SaveHighScore (HighScore = Score); Score = 0; Multiplicateur = 1; Vies = 4; scoreForExtraLife = 2000; multiplierTimeLeft = 0;
Cela permet de suivre le meilleur score. Nous devons maintenant l'afficher. Ajoutez le code suivant à GameRoot.Draw ()
dans le même SpriteBatch
bloc où l'autre texte est dessiné:
if (PlayerStatus.IsGameOver) string text = "Game Over \ n" + "Votre score:" + PlayerStatus.Score + "\ n" + "Meilleur score:" + PlayerStatus.HighScore; Vector2 textSize = Art.Font.MeasureString (text); spriteBatch.DrawString (Art.Font, text, ScreenSize / 2 - textSize / 2, Color.White);
Cela fera apparaître votre score et votre meilleur score en fin de partie, centrés à l'écran.
Pour un ajustement final, nous allons augmenter le temps qui s'écoule avant que le navire ne revienne au jeu afin de donner au joueur le temps de voir son score. Modifier PlayerShip.Kill ()
en réglant le temps de réapparition à 300 images (cinq secondes) si le joueur est à court de vies.
// dans PlayerShip.Kill () PlayerStatus.RemoveLife (); framesUntilRespawn = PlayerStatus.IsGameOver? 300: 120;
Le jeu est maintenant prêt à jouer. Cela peut ne pas sembler beaucoup, mais il a tous les mécanismes de base mis en œuvre. Dans les prochains tutoriels, nous ajouterons un filtre bloom et des effets de particules pour le pimenter. Mais pour le moment, ajoutons rapidement du son et de la musique pour le rendre plus intéressant.
Jouer du son et de la musique est facile avec XNA. Premièrement, nous ajoutons nos effets sonores et notre musique au pipeline de contenu. dans le Propriétés dans le volet, assurez-vous que le processeur de contenu est défini sur Chanson
pour la musique et Effet sonore
pour les sons.
Ensuite, nous faisons une classe d'assistance statique pour les sons.
classe statique Sound public statique Song Music get; ensemble privé; private static readonly Random rand = new Random (); explosions privées SoundEffect []; // retourne une explosion aléatoire son publique statique SoundEffect Explosion get return explosions [rand.Next (explosions.Length)]]; privé statique SoundEffect [] coups; public statique SoundEffect Shot get return shots [rand.Next (shots.Length)]]; privé statique SoundEffect [] spawns; public statique SoundEffect Spawn get return spawns [rand.Next (spawns.Length)]]; public static void Load (contenu ContentManager) Music = content.Load("Son / Musique"); // Ces expressions linq ne sont qu'un moyen sophistiqué de charger tous les sons de chaque catégorie dans un tableau. explosions = Enumerable.Range (1, 8) .Select (x => content.Load ("Sound / explosion-0" + x)). ToArray (); shots = Enumerable.Range (1, 4) .Select (x => content.Load ("Son / shoot-0" + x)). ToArray (); spawns = Enumerable.Range (1, 8) .Select (x => content.Load ("Sound / spawn-0" + x)). ToArray ();
Puisque nous avons plusieurs variations de chaque son, le Explosion
, Coup
, et Frayer
les propriétés choisiront un son au hasard parmi les variantes.
Appel Sound.Load ()
dans GameRoot.LoadContent ()
. Pour jouer la musique, ajoutez les deux lignes suivantes à la fin de GameRoot.Initialize ()
.
MediaPlayer.IsRepeating = true; MediaPlayer.Play (Sound.Music);
Pour jouer des sons dans XNA, vous pouvez simplement appeler le Jouer()
méthode sur un Effet sonore
. Cette méthode fournit également une surcharge qui vous permet de régler le volume, la hauteur et le panoramique du son. Une astuce pour rendre nos sons plus variés consiste à ajuster ces quantités à chaque jeu..
Pour déclencher l'effet sonore lors de la prise de vue, ajoutez la ligne suivante dans PlayerShip.Update ()
, à l'intérieur de l'instruction if où les puces sont créées. Notez que nous décalons aléatoirement la hauteur vers le haut ou le bas, jusqu'à un cinquième d'octave, pour rendre les sons moins répétitifs..
Sound.Shot.Play (0.2f, rand.NextFloat (-0.2f, 0.2f), 0);
De même, déclenchez un effet sonore d'explosion chaque fois qu'un ennemi est détruit en ajoutant ce qui suit à Enemy.WasShot ()
.
Sound.Explosion.Play (0.5f, rand.NextFloat (-0.2f, 0.2f), 0);
Vous avez maintenant du son et de la musique dans votre jeu. Facile, n'est-ce pas?
Cela met fin aux mécanismes de base du gameplay. Dans le prochain tutoriel, nous allons ajouter un filtre bloom pour faire briller les néons..