MonoAsteroids – Del 5

Inledning

Denna artikel är en direkt fortsättning på artikeln MonoAsteroids - Del 4 . Nu är det dags att färdigställa spelet och göra det "klart".

Artikeln är mer ett stöd till den videogenomgång som mer steg för steg går igenom alla moment. Videogenomgången hittar du i slutet av artikeln.

SpriteFont

I denna sista och avslutande del så behövs en ny textur som ska fungera som typsnitt. Du hittar den här nedan. Glöm ej bara att sätta Processor till "Font Texture" i Pipeline-verktyget.

Ändrade filer

Globals.cs

using Microsoft.Xna.Framework;

namespace MonoAsteroids
{
    class Globals
    {
        public static int ScreenWidth = 1280;
        public static int ScreenHeight = 720;

        public static Rectangle GameArea
        {
            get
            {
                return new Rectangle(-80, -80, ScreenWidth + 160, ScreenHeight + 160);
            }
        }

        public static Rectangle RespawnArea
        {
            get
            {
                return new Rectangle((int)CenterScreen.X - 200, (int)CenterScreen.Y - 200, 400, 400);
            }
        }

        public static Vector2 CenterScreen
        {
            get { return new Vector2(ScreenWidth / 2, ScreenHeight / 2); }
        }
    }
}
GameObjectManager.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using MonoAsteroids.Messanges;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MonoAsteroids
{
    class GameObjectManager : DrawableGameComponent
    {
        Random rnd = new Random();

        List<Meteor> meteors = new List<Meteor>();
        Texture2D meteorBigTexture;
        Texture2D meteorMediumTexture;
        Texture2D meteorSmallTexture;

        List<Shot> shots = new List<Shot>();
        Texture2D laserTexture;

        SoundEffect laserSound;
        SoundEffect explosionSound;
        Texture2D explosionTexture;
        List<Explosion> explosions = new List<Explosion>();

        public GameObjectManager(Game game) : base(game)
        {

        }

        public override void Initialize()
        {
            Messenger.Instance.Register<AddShotMessage>(this, AddShotMessageCallback);
            base.Initialize();
        }

        public void Reset()
        {
            meteors.Clear();
            shots.Clear();
            explosions.Clear();

            while (meteors.Count < 10)
            {
                var angle = rnd.Next() * MathHelper.TwoPi;
                var m = new Meteor(MeteorType.Big)
                {
                    Position = new Vector2(Globals.GameArea.Left + (float)rnd.NextDouble() * Globals.GameArea.Width,
                        Globals.GameArea.Top + (float)rnd.NextDouble() * Globals.GameArea.Height),
                    Rotation = angle,
                    Speed = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * rnd.Next(20, 60) / 30.0f
                };

                if (!Globals.RespawnArea.Contains(m.Position))
                    meteors.Add(m);
            }
        }

        private void AddShotMessageCallback(AddShotMessage msg)
        {
            shots.Add(msg.Shot);
            laserSound.Play();
        }

        protected override void LoadContent()
        {
            laserTexture = Game.Content.Load<Texture2D>("laser");
            meteorBigTexture = Game.Content.Load<Texture2D>("meteorBrown_big4");
            meteorMediumTexture = Game.Content.Load<Texture2D>("meteorBrown_med1");
            meteorSmallTexture = Game.Content.Load<Texture2D>("meteorBrown_tiny1");

            laserSound = Game.Content.Load<SoundEffect>("laserSound");
            explosionSound = Game.Content.Load<SoundEffect>("explosionSound");
            explosionTexture = Game.Content.Load<Texture2D>("explosion");

            base.LoadContent();
        }

        public void CheckPlayerCollision(Player playerComponent)
        {
            var collidingMeteor = meteors.FirstOrDefault(m => m.CollidesWith(playerComponent));
            if (collidingMeteor != null)
            {
                explosionSound.Play(collidingMeteor.ExplosionScale, 0.0f, 0.0f);
                playerComponent.Lifes--;
                meteors.Remove(collidingMeteor);
                explosions.Add(new Explosion()
                {
                    Position = collidingMeteor.Position,
                    Rotation = collidingMeteor.Rotation,
                    Scale = collidingMeteor.ExplosionScale
                });

                if (playerComponent.Lifes <= 0)
                    Messenger.Instance.Send(new GameStateChangedMessage() { NewState = GameState.Dead });
            }
        }

        public override void Update(GameTime gameTime)
        {
            foreach (Shot shot in shots)
            {
                shot.Update(gameTime);
                Meteor meteor = meteors.FirstOrDefault(m => m.CollidesWith(shot));

                if (meteor != null)
                {
                    meteors.Remove(meteor);
                    meteors.AddRange(Meteor.BreakMeteor(meteor));
                    explosions.Add(new Explosion()
                    {
                        Position = meteor.Position,
                        Scale = meteor.ExplosionScale
                    });
                    shot.IsDead = true;
                    explosionSound.Play(0.7f, 0f, 0f);
                }
            }

            foreach (Explosion explosion in explosions)
                explosion.Update(gameTime);

            foreach (Meteor metor in meteors)
                metor.Update(gameTime);

            shots.RemoveAll(s => s.IsDead || !Globals.GameArea.Contains(s.Position));
            explosions.RemoveAll(e => e.IsDead);

            if (meteors.Count == 0)
                Messenger.Instance.Send(new GameStateChangedMessage() { NewState = GameState.Won });

            base.Update(gameTime);
        }

        public void Draw(SpriteBatch spriteBatch)
        {
            foreach (Shot s in shots)
            {
                spriteBatch.Draw(laserTexture, s.Position, null, Color.White, s.Rotation,
                new Vector2(laserTexture.Width / 2, laserTexture.Height / 2), 1.0f, SpriteEffects.None, 0f);
            }

            foreach (Meteor meteor in meteors)
            {
                Texture2D meteorTexture = meteorSmallTexture;

                switch (meteor.Type)
                {
                    case MeteorType.Big: meteorTexture = meteorBigTexture; break;
                    case MeteorType.Medium: meteorTexture = meteorMediumTexture; break;
                }

                spriteBatch.Draw(meteorTexture, meteor.Position, null, Color.White, meteor.Rotation,
                new Vector2(meteorTexture.Width / 2, meteorTexture.Height / 2), 1.0f, SpriteEffects.None, 0f);
            }

            foreach (Explosion explosion in explosions)
            {
                spriteBatch.Draw(explosionTexture, explosion.Position, null, explosion.Color, explosion.Rotation,
                                new Vector2(explosionTexture.Width / 2, explosionTexture.Height / 2), explosion.Scale, SpriteEffects.None, 0f);
            }
        }
    }
}

MonoAsteroids.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoAsteroids.Messanges;
using System;
using System.Collections.Generic;
using System.Linq;

namespace MonoAsteroids
{
    public class MonoAsteroids : Game
    {
        GameState state;
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        Texture2D backgroundTexture;
        SpriteFont fontTexture;

        Player player;
        GameObjectManager gameObjectManager;
        KeyboardState previousKbState;

        public MonoAsteroids()
        {
            graphics = new GraphicsDeviceManager(this);
            graphics.PreferredBackBufferHeight = Globals.ScreenHeight;
            graphics.PreferredBackBufferWidth = Globals.ScreenWidth;

            Content.RootDirectory = "Content";
        }

        protected override void Initialize()
        {
            player = new Player(this);
            Components.Add(player);
            gameObjectManager = new GameObjectManager(this);
            Components.Add(gameObjectManager);

            Messenger.Instance.Register<GameStateChangedMessage>(this, OnGameStateChangedCallback);
            Messenger.Instance.Send(new GameStateChangedMessage() { NewState = GameState.GetReady });
            base.Initialize();
        }

        private void OnGameStateChangedCallback(GameStateChangedMessage msg)
        {
            if (msg.NewState == state)
                return;

            switch(msg.NewState)
            {
                case GameState.GetReady:
                    gameObjectManager.Reset();
                    player.Reset();
                    player.Enabled = gameObjectManager.Enabled = false;
                    break;
                case GameState.Playing:
                    player.Enabled = gameObjectManager.Enabled = true;
                    break;
                case GameState.Dead:
                case GameState.Won:
                    player.Enabled = gameObjectManager.Enabled = false;
                    break;
            }

            state = msg.NewState;
        }

        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            backgroundTexture = Content.Load<Texture2D>("background");
            fontTexture = Content.Load<SpriteFont>("spriteFontBig");
        }

        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            KeyboardState kbState = Keyboard.GetState();

            switch(state)
            {
                case GameState.Dead:
                case GameState.Won:
                    if (kbState.IsKeyDown(Keys.Space) && previousKbState.IsKeyUp(Keys.Space))
                        Messenger.Instance.Send(new GameStateChangedMessage() { NewState = GameState.GetReady });
                    break;
                case GameState.GetReady:
                    if (kbState.IsKeyDown(Keys.Space) && previousKbState.IsKeyUp(Keys.Space))
                        Messenger.Instance.Send(new GameStateChangedMessage() { NewState = GameState.Playing });
                    break;
                case GameState.Playing:
                    gameObjectManager.CheckPlayerCollision(player);
                    break;
            }

            
            previousKbState = kbState;

            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            spriteBatch.Begin();

            for (int y = 0; y < Globals.ScreenHeight; y += backgroundTexture.Width)
            {
                for (int x = 0; x < Globals.ScreenWidth; x += backgroundTexture.Width)
                {
                    spriteBatch.Draw(backgroundTexture, new Vector2(x, y), Color.White);
                }
            }

            gameObjectManager.Draw(spriteBatch);
            player.Draw(spriteBatch);

            string overlayText = null;

            switch(state)
            {
                case GameState.GetReady:
                    overlayText = "Get ready! Press space!";
                    break;
                case GameState.Dead:
                    overlayText = "GAME OVER!";
                    break;
                case GameState.Won:
                    overlayText = "Congratulations!";
                    break;
            }

            if (!string.IsNullOrEmpty(overlayText))
            {
                var size = fontTexture.MeasureString(overlayText);
                spriteBatch.DrawString(fontTexture, overlayText, Globals.CenterScreen - size/2.0f, Color.DarkViolet);
            }

            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}
Player.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoAsteroids.Messanges;
using System;

namespace MonoAsteroids
{
    class Player : DrawableGameComponent, IGameObject
    {
        public bool IsDead { get; set; }
        public int Lifes { get; set; }
        public Vector2 Position { get; set; }
        public float Radius { get; set; }
        public Vector2 Speed { get; set; }
        public float Rotation { get; set; }

        public bool CanShoot { get { return reloadTimer == 0; } }

        private Texture2D playerTexture;
        private Texture2D lifeTexture;
        private int reloadTimer = 0;
        private Random rnd = new Random();

        public Player(Game game) : base(game)
        {
            Radius = 30;
            Reset();
        }

        protected override void LoadContent()
        {
            playerTexture = Game.Content.Load<Texture2D>("player");
            lifeTexture = Game.Content.Load<Texture2D>("life");

            base.LoadContent();
        }

        public void Draw(SpriteBatch spriteBatch)
        {
            spriteBatch.Draw(playerTexture, Position, null, Color.White, Rotation + MathHelper.PiOver2,
                new Vector2(playerTexture.Width / 2, playerTexture.Height / 2), 1.0f, SpriteEffects.None, 0f);

            for(int i=0; i<Lifes; i++)
            {
                spriteBatch.Draw(lifeTexture, new Vector2(40 + i * 50, 40), Color.White);
            }
        }

        public override void Update(GameTime gameTime)
        {
            KeyboardState state = Keyboard.GetState();

            if (state.IsKeyDown(Keys.Up))
                Accelerate();
            if (state.IsKeyDown(Keys.Left))
                Rotation -= 0.05f;
            else if (state.IsKeyDown(Keys.Right))
                Rotation += 0.05f;

            if (state.IsKeyDown(Keys.Space) && CanShoot)
                Messenger.Instance.Send(new AddShotMessage() { Shot = Shoot() });

            Position += Speed;

            if (reloadTimer > 0)
                reloadTimer--;

            if (Position.X < Globals.GameArea.Left)
                Position = new Vector2(Globals.GameArea.Right, Position.Y);
            if(Position.X > Globals.GameArea.Right)
                Position = new Vector2(Globals.GameArea.Left, Position.Y);
            if (Position.Y < Globals.GameArea.Top)
                Position = new Vector2(Position.X, Globals.GameArea.Bottom );
            if (Position.Y > Globals.GameArea.Bottom)
                Position = new Vector2(Position.X, Globals.GameArea.Top);

            base.Update(gameTime);
        }

        public void Reset()
        {
            Speed = Vector2.Zero;
            Lifes = 3;
            Position = Globals.CenterScreen;   
        }

        public void Accelerate()
        {
            Speed += new Vector2((float)Math.Cos(Rotation), 
                (float)Math.Sin(Rotation))* 0.10f;

            if(Speed.LengthSquared() > 16)
                Speed = Vector2.Normalize(Speed) * 4;
        }

        public Shot Shoot()
        {
            if (!CanShoot)
                return null;

            reloadTimer = 20;

            return new Shot()
            {
                Position = Position,
                Speed = Speed + 10f * new Vector2((float)Math.Cos(Rotation), (float)Math.Sin(Rotation)),
                Rotation = rnd.Next() * MathHelper.TwoPi
            };
        }
    }
}

Videogenomgång

Övningar

Här följer lite tips/övningar på fortsatt arbete om du vill utveckla spelet vidare!

Meteor HP

Det vore logiskt om meteoriterna tålde olika mycket innan de sprängdes. T.ex. att en stor tålde 4 skott, en medel 2 och en liten 1.

Power ups

Lägg till en chans att det dyker upp en "power up" när meteoriterna exploderar. Förslag på olika power ups:
  • Liv
  • Ökad acceleration
  • Nya/kraftigare vapen

Teleportering

Möjligheten att snabbt förflytta sig från fara genom att tryck "bakåt" (pil ned). Skeppet ska då förflytta sig till en ledig plats på skärmen.

Tidmätning

Inför tidmätning i sekunder. På så vis kan man även skapa en "high score" som sparas, dvs. bästa tiden att slå.

Levels

Inför ökad svårighetsgrad efter varje avklarad omgång. T.ex. så skulle "level 1" kanske bara innehålla 3 meteoriter. "Level 2" fem meteoriter som rör sig snabbare, etc.

Lämna ett svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *

Scroll to top