📜 ⬆️ ⬇️

Getting to know XNA and writing the first music game

Hello to all novice game developers and just good people. Today, I want to introduce you to the wonderful XNA framework (the dotNet managed runtime toolkit). We will program in C #.
In order to introduce you to XNA closer, I propose to write a simple "musical" 2D toy. The rest is under the cut.


Short description on wikipedia

Microsoft XNA ( XNA's Not Acronymed ) is a set of tools with a controlled runtime environment (.NET) created by Microsoft that makes it easy to develop and manage computer games. XNA seeks to free game development from writing recurring pattern code.

What do we need for this?

1) Fresh DirectX ( for example June 2010 )
2) Microsoft Visual C # 2010 EXPRESS ( free license )
3) Microsoft XNA Game Studio 4.0
')
What is supposed to be dismantled and done in this lesson?



What game will we implement?

The mechanics of the game is simple to madness. The meaning will be built on the music, in the case of this game, the composition of Isaac Shepard - Leaves in the Wind will be used. It will be necessary to catch the mouse "notes", the speed and number of which will be dependent on the current position in the music, roughly speaking, the game "visualizer". For variety, there are 5 types of notes: normal , red (enemies), purple (power), blinking (turns everything into yellow), yellow (increases the speed of scoring and size).

Packing up an empty project

To begin with, we put all the necessary components in order, then we start Microsoft Visual C # 2010 EXPRESS and create the Windows Game (4.0) project and call it music_catch:

An empty project is created, which, when compiled, only cleans up the “screen” of the application, let's take a closer look at the structure of the new project.


The music_catch project is the “logic” of our application.
Game1.cs is the main class of the application, inherited from Microsoft.Xna.Framework.Game
Program.cs - “entry point” in the application, it is not interesting to us.
The music_catchContent project is the “content” of our application, where we will add resources.

Take a closer look at Game1.cs
It can highlight the main features, such as:
Game1 () is a class constructor.
Initialize () - initialization of the application.
LoadContent () - loading content.
UnloadContent () - download content.
Update (GameTime gameTime) - update application logic (for example physics, etc)
Draw (GameTime gameTime) - drawing games. ATTENTION, any drawing operations should be carried out here and only here.

The empty project is assembled, we go further, we add resources to the application, we “throw” all the necessary resources into the music_catch \ music_catchContent folder. In our case - five PNG files and one musical accompaniment. Add this all to the project:



In the same place we create a font, in the body of SpriteFont1.spritefont we specify the name and size:
<FontName>Segoe UI Mono</FontName> <Size>14</Size> 




Create variables for future content:
 private List<Texture2D> MelList; private Texture2D mouse; private Song song; private SpriteFont font; 


And we load it into LoadContent ():

 MelList = new List<Texture2D>(); for(int a = 1; a <= 5; a++) MelList.Add(Content.Load<Texture2D>("mel" + a)); mouse = Content.Load<Texture2D>("mouse"); song = Content.Load<Song>("Leaves_in_the_Wind"); font = Content.Load<SpriteFont>("SpriteFont1"); 


By the way, the content is loaded as follows: Content.Load <> ("asset") is called;
The content processor is indicated in triangular brackets, in our case it is Texture2D, Song, SpriteFont. You can use your processors, I'll tell you about it later.

Content is loaded, go to the Game1 constructor () and write:
 graphics = new GraphicsDeviceManager(this); graphics.PreferredBackBufferWidth = 800; //   graphics.PreferredBackBufferHeight = 600; //   graphics.IsFullScreen = false; //    graphics.ApplyChanges(); //   Content.RootDirectory = "Content"; 


The application is initialized.

Writing "game logic"


Now we need to create a controller of the particle system and the particles themselves (notes), which we will masterfully catch with the mouse.
We create two classes: Catcher (the particles themselves) and CatcherHolder (particle system).

Listing Catcher with comments:
 using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Media; namespace MusicCatch { public class Catcher { public Texture2D Texture { get; set; } //   public Vector2 Position { get; set; } //   public Vector2 Velocity { get; set; } //   public float Angle { get; set; } //    public float AngularVelocity { get; set; } //    public Color Color { get; set; } //   public float Size { get; set; } //   public int TTL { get; set; } //    private float RComponent; //   RGB private float GComponent; //   RGB private float BComponent; //   RGB public int type; //   private Random random; //    public Catcher(Texture2D texture, Vector2 position, Vector2 velocity, float angle, float angularVelocity, int type, float size, int ttl) { //     Texture = texture; Position = position; Velocity = velocity; Angle = angle; AngularVelocity = angularVelocity; this.type = type; Size = size; TTL = ttl; SetType(type); //      } public void ApplyImpulse(Vector2 vector) //   ( ) { Velocity += vector; } public void Update() //    { TTL--; Position += Velocity; Angle += AngularVelocity; if (type != -1) { Velocity = new Vector2(Velocity.X, Velocity.Y - .1f); Size = (10 + Velocity.Y) / 20; if(Size > 0.8f) Size = 0.8f; } if (type == 0) { GComponent -= 0.005f; BComponent += 0.005f; Color = new Color(RComponent, GComponent, BComponent); } else if (type == 4) { Color = new Color((float)(1f * random.NextDouble()), (float)(1f * random.NextDouble()), (float)(1f * random.NextDouble())); } } public void Draw(SpriteBatch spriteBatch) //   { Rectangle sourceRectangle = new Rectangle(0, 0, Texture.Width, Texture.Height); Vector2 origin = new Vector2(Texture.Width / 2, Texture.Height / 2); spriteBatch.Draw(Texture, Position, sourceRectangle, Color, Angle, origin, Size, SpriteEffects.None, 0f); } public void SetType(int type) //    { this.type = type; Color StartColor = new Color(1f, 1f, 1f); switch (type) { case 0: StartColor = new Color(0f, 1f, 0f); break; //  case 1: StartColor = new Color(1f, 0f, 0f); break; //  case 2: StartColor = new Color(1f, 0f, 1f); break; //  case 3: StartColor = new Color(1f, 1f, 0f); break; //  case 4: random = new Random(); break; //  } RComponent = ((int)StartColor.R) / 255f; GComponent = ((int)StartColor.G) / 255f; BComponent = ((int)StartColor.B) / 255f; Color = new Color(RComponent, GComponent, BComponent); if (type == -1) { Color = new Color(1f, 1f, 1f, 0.1f); } } } } 


Listing CatcherHolder with comments:
 using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Media; namespace MusicCatch { class CatcherHolder { private Random random; //    public List<Catcher> particles; //   (Catcher) private List<Texture2D> textures; //   public List<float> accomulator { get; set; } //  float-,      accomulator —   . public CatcherHolder(List<Texture2D> textures) { this.textures = textures; this.particles = new List<Catcher>(); random = new Random(); accomulator = new List<float>(); //       128  — 1.0f for (int a = 0; a < 128; a++) { accomulator.Add(1.0f); } } //    // Wave - ,   0f   . private Catcher GenerateNewParticle(float Wave) { Texture2D texture = textures[random.Next(textures.Count)]; //      Vector2 position = new Vector2(Wave, 0); //   Vector2 velocity = new Vector2((float)(random.NextDouble() - 0.5), (float)(random.NextDouble() * 10)); //  , 0.5f  X  10f  Y float angle = 0; //   = 0 float angularVelocity = 0.05f * (float)(random.NextDouble()*2 - 1 ); //    Color color = new Color(0f, 1f, 0f); //   (     Catcher) float size = (float)random.NextDouble()*.8f + .2f; //   int ttl = 400; //    400 (400    , .. 400 / 60 — 6   . int type = 0; —   0 //   if (random.Next(10000) > 9900) //  type = 1; else if (random.Next(10000) > 9950) //  type = 3; else if (random.Next(10000) > 9997) //  type = 2; else if (random.Next(10000) > 9998) //  type = 4; return new Catcher(texture, position, velocity, angle, angularVelocity, type, size, ttl); //      } //         public void GenerateYellowExplossion(int x, int y, int radius) { Texture2D texture = textures[random.Next(textures.Count)]; Vector2 direction = Vector2.Zero; float angle = (float)Math.PI * 2.0f * (float)random.NextDouble(); float length = radius * 4f; direction.X = (float)Math.Cos(angle); direction.Y = -(float)Math.Sin(angle); Vector2 position = new Vector2(x, y) + direction * length; Vector2 velocity = direction * 4f; float angularVelocity = 0.05f * (float)(random.NextDouble() * 2 - 1); float size = (float)random.NextDouble() * .8f + .2f; int ttl = 400; int type = 3; particles.Add(new Catcher(texture, position, velocity, 0, angularVelocity, type, size, ttl)); } // "" ,   public void Beat(float Wave) { particles.Add(GenerateNewParticle(Wave)); } public void Update() //    { for (int particle = 0; particle < particles.Count; particle++) { particles[particle].Update(); if (particles[particle].Size <= 0 || particles[particle].TTL <= 0) { //        ,   particles.RemoveAt(particle); particle--; } } //  ,     1f,   ,     Constants — ACCUMULATE_SPEED,  Constanst - . for (int a = 0; a < 128; a++) if (accomulator[a] < 1.0f) accomulator[a] += Constanst.ACCUMULATE_SPEED; } public void Draw(SpriteBatch spriteBatch) { //   ,   BlendState.Additive,     "". spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Additive); for (int index = 0; index < particles.Count; index++) { particles[index].Draw(spriteBatch); } spriteBatch.End(); } } } 


Listing Constant.cs:
 using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace MusicCatch { public class Constanst { public const float BEAT_COST = .4f; // "" ,        public const float ACCUMULATE_SPEED = .01f; //   public const float BEAT_REACTION = .5f; //    ""   public const float ACCOMULATOR_REACTION = .5f; //      ,      } } 


I will explain what a mysterious battery is and why it is needed. Let's talk about the "musical" spectrum.

The music signal is food for the audio system. More precisely - not so. The speakers do not listen to music, our brain restores it, receiving a complex signal containing many frequency components.
So, the idea is to listen to the “frequencies” of each Update and write them to some, for example, VisualizationData. Simply put, in an array of 128 elements that vary from 0f to 1f.

How can this be used?
Each Update: the values ​​in the array change in accordance with the music, we need to check all 128 elements, if the value of the element is greater than 0.6f, call the Beat function and pass the Wave to it (the index of the array element in which the event occurred). Everything is good, you can create a particle note in Beat. But imagine that we have three Update'a in a row, in which in the same index - value> 0.6f, as a result there will be 100500 particles per second. To prevent such things from happening, you can use a battery. Its meaning is simple: with Beat, the BEAT_COST constant is subtracted from the cell of the battery array of the corresponding Wave index. Each Update adds ACCUMULATE_SPEED to all battery cells. Before calling Beat it is checked whether the condition is satisfied - the value of the battery> ACCOMULATOR_REACTION, if yes, then call the Beat. This solves the problem.

By the way, BEAT_REACTION is the value after which you need to check whether it is worth calling the Beat.

Further I will give the full listing of GameLogic (Game1). A lot of code, but I will try to write in the comments.
 using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Media; namespace MusicCatch { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; private List<Texture2D> MelList; private Texture2D mouse; private CatcherHolder m_cHolder; MediaLibrary mediaLibrary; //   "" Song song; //   VisualizationData visualizationData; SpriteFont font; private int scores = 0; //  private float self_size = 1f; //  "" private int xsize = 1; //   private float power = 0f; //     private float activity = 0f; //     public Game1() { graphics = new GraphicsDeviceManager(this); graphics.PreferredBackBufferWidth = 800; graphics.PreferredBackBufferHeight = 600; graphics.IsFullScreen = false; graphics.ApplyChanges(); Content.RootDirectory = "Content"; //   mediaLibrary = new MediaLibrary(); visualizationData = new VisualizationData(); scores = 0; } protected override void Initialize() { m_cHolder = new CatcherHolder(MelList); MediaPlayer.Play(song); //    MediaPlayer.IsVisualizationEnabled = true; //   base.Initialize(); } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); MelList = new List<Texture2D>(); for(int a = 1; a <= 5; a++) MelList.Add(Content.Load<Texture2D>("mel" + a)); mouse = Content.Load<Texture2D>("mouse"); song = Content.Load<Song>("Leaves_in_the_Wind"); font = Content.Load<SpriteFont>("SpriteFont1"); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { m_cHolder.Update(); MediaPlayer.GetVisualizationData(visualizationData); //   // ""   ,   for (int a = 0; a < 128; a++) { if (visualizationData.Frequencies[a] > Constanst.BEAT_REACTION && m_cHolder.accomulator[a] > Constanst.ACCOMULATOR_REACTION) { m_cHolder.Beat(a * 3.125f * 2); //  "",   . m_cHolder.accomulator[a] -= Constanst.BEAT_COST; //   } } // ,   ,       if (power > 0f) { for (int particle = 0; particle < m_cHolder.particles.Count; particle++) { if (m_cHolder.particles[particle].type != 1) //   ,   { float body1X = m_cHolder.particles[particle].Position.X; float body1Y = m_cHolder.particles[particle].Position.Y; float body2X = (float)Mouse.GetState().X; float body2Y = (float)Mouse.GetState().Y; float Angle = (float)Math.Atan2(body2X - body1X, body2Y - body1Y) - ((float)Math.PI / 2.0f); //     float Lenght = (float)(5000f * power) / (float)Math.Pow((float)Distance(body1X, body1Y, body2X, body2Y), 2.0f); //   m_cHolder.particles[particle].ApplyImpulse(AngleToV2(Angle, Lenght)); //    } } power -= 0.001f; //   } activity -= 0.001f; //    if (activity < 0.0f) activity = 0.0f; else if (activity > 0.5f) activity = 0.5f; //     0f  .5f //    :    for (int particle = 0; particle < m_cHolder.particles.Count; particle++) { int x = (int)m_cHolder.particles[particle].Position.X; int y = (int)m_cHolder.particles[particle].Position.Y; int radius = (int)(16f * m_cHolder.particles[particle].Size); if (circlesColliding(Mouse.GetState().X, Mouse.GetState().Y, (int)(16f * self_size), x, y, radius)) { scores += (int)(10f * m_cHolder.particles[particle].Size * xsize); //  ,        activity += 0.005f; //   int type = m_cHolder.particles[particle].type; //   ,     switch (type) { case 3: //  self_size += 0.1f; xsize += 1; //      if (self_size > 4.0f) self_size = 4.0f; break; case 2: //  power = 1f; //   ,       break; case 4: //  for (int b = 0; b < m_cHolder.particles.Count; b++) m_cHolder.particles[b].SetType(3); //     —  break; case 1: //  () for(int a = 1; a < xsize; a++) m_cHolder.GenerateYellowExplossion(Mouse.GetState().X, Mouse.GetState().Y, (int)(16f * self_size)); xsize = 1; self_size = 1f; scores -= (int)(scores / 4); break; } //   m_cHolder.particles[particle].TTL = 0; m_cHolder.particles.RemoveAt(particle); particle--; } } base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Black); m_cHolder.Draw(spriteBatch); //  CatcherHolder spriteBatch.Begin(); Rectangle sourceRectangle = new Rectangle(0, 0, mouse.Width, mouse.Height); //   Vector2 origin = new Vector2(mouse.Width / 2, mouse.Height / 2); // offset  Vector2 mouse_vector = new Vector2(Mouse.GetState().X, Mouse.GetState().Y); // ()  string xtext = "x" + xsize.ToString(); //  Vector2 text_vector = font.MeasureString(xtext) / 2.0f; //  offset'a  spriteBatch.Draw(mouse, mouse_vector, sourceRectangle, new Color(0.5f - power/2.0f + activity, 0.5f, 0.5f - power/2.0f), 0.0f, origin, self_size, SpriteEffects.None, 0f); //   spriteBatch.DrawString(font, xtext, mouse_vector - text_vector, Color.White); //   spriteBatch.DrawString(font, "Score: " + scores.ToString(), new Vector2(5, graphics.PreferredBackBufferHeight - 34), Color.White); //   spriteBatch.End(); base.Draw(gameTime); } // ,       bool circlesColliding(int x1, int y1, int radius1, int x2, int y2, int radius2) { int dx = x2 - x1; int dy = y2 - y1; int radii = radius1 + radius2; if ((dx * dx) + (dy * dy) < radii * radii) { return true; } else { return false; } } //      public Vector2 AngleToV2(float angle, float length) { Vector2 direction = Vector2.Zero; direction.X = (float)Math.Cos(angle) * length; direction.Y = -(float)Math.Sin(angle) * length; return direction; } //  public float Distance(float x1, float y1, float x2, float y2) { return (float)Math.Sqrt((float)Math.Pow(x2 - x1, 2) + (float)Math.Pow(y2 - y1, 2)); } } } 


This is such a simple toy. XNA 4.0 and .NET must be installed on the user's end machine;

Links: the game itself ( direct ) | source ( direct ) | XNA Framework End-user

Screenshot:


PS The idea is not mine, this game has already been released under flash . This game was written exclusively for the article, it will not receive further development accordingly.
PSS I will also help to understand the XNA / lesson, for this, write to me in a personal on Habré or on contacts that are in the profile.

Source: https://habr.com/ru/post/128112/


All Articles