MonoAsteroids – Del 4

Inledning

Denna artikel är en direkt fortsättning på artikeln MonoAsteroids - Del 3 . Det är nu dags för en storstädning samt att lägga till meddelandehantering och Game States. Vi tittar även lite närmare på Mediator designmönstret.

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.

Mediator

Ett designmässigt problem uppstår om Player ska bestämma när nya skott ska produceras. Plötsligt måste Player känna till eller på något vis signalera spelet att sett skott ska läggas till.

Inom programmering finns en term som heter design patterns. Det är mönster som återkommer inom programmering som löser ett vanligt förekommande problem. Det problem vi har kan lösas med ett design pattern som heter Mediator.

Tanken med Mediator är att två objekt kan kommunicera med varanda utan att varken känna till eller vara beroende av varandra. Systemet fungerar som ett meddelandesystem där men prenumererar på meddelanden av en viss sort och sedan, vid behov, kan skicka meddelanden.

I vår omplementation så har vi valt att använda ett enklare designmönster som heter Singleton. Med singleton så såkerställer vi att det bara finns en instans av klassen som vi når via den statiska klassmedlemmen Instance. Vi implementerar Mediator i en klass vi kallar Messenger som främst erbjuder metoderna Send och Register.

Mer info om Mediator kommer i en separat artikel. Vi vill samtidigt varna för att vår implementation är rätt enkel och är varken trådsäker eller speciellt resurssäker.

Messenger.cs

using System;
using System.Collections.Generic;
using System.Reflection;

namespace MonoAsteroids
{
    public class Messenger
    {
        Dictionary<Type, List<RecipientAndMethod>> _registered = 
			new Dictionary<Type, List<RecipientAndMethod>>();

        private void Messanger()
        {
        }

        #region Properties

        private static Messenger _instance;

        public static Messenger Instance
        {
            get
            {
                if (_instance == null)
                    _instance = new Messenger();
                return _instance;
            }
        }

        #endregion

        #region Public Methods

        public void Send<TMessage>(TMessage msg)
        {
            Type type = typeof(TMessage);

            if (!_registered.ContainsKey(type))
                return;

            var list = _registered[type];

            foreach (var recipientAndMethod in list)
            {
                recipientAndMethod.MethodInfo.Invoke(
					recipientAndMethod.Recipient, new object[] { msg });
            }
        }

        public void Register<TMessage>(object recipient, Action<TMessage> action)
        {
            Type type = typeof(TMessage);

            List<RecipientAndMethod> list = (_registered.ContainsKey(type)) 
				? _registered[type] : new List<RecipientAndMethod>();

            RecipientAndMethod rm = new RecipientAndMethod()
            {
                MethodInfo = action.Method,
                Recipient = recipient
            };
            list.Add(rm);

            if (!_registered.ContainsKey(type))
                _registered.Add(type, list);
        }

        #endregion

        #region Private Struct

        private struct RecipientAndMethod
        {
            public MethodInfo MethodInfo { get; set; }
            public object Recipient { get; set; }
        }

        #endregion

    }
}

Meddelanden

Vi skapar också klasser som beskriver olika sorters meddelanden och som innehåller den data som är viktig för respektive meddelande.

Vi behöver ett meddelande för att signalera nya skott som vi skapar med klassen AddShotMessage. Sen behöver vi ett meddelande för att signalera ändring på spelets tillstånd via klassen GameStateChangedMessage.

AddShotMessage.cs

namespace MonoAsteroids.Messanges
{
    class AddShotMessage
    {
        public Shot Shot { get; set; }
    }
}
GameStateChangedMessage.cs

namespace MonoAsteroids.Messanges
{
    class GameStateChangedMessage
    {
        public GameState NewState { get; set; }
    }
}

Game States

Innan vi har ett komplett spel så måste vi kunna tillåta att spelet befinner sig i olika tillstånd, sk. states. Först ser vi till att skapa en enum som beskriver i vilka olika tillstånd spelet kan befinna sig i.

GameState.cs

namespace MonoAsteroids
{
    enum GameState
    {
        Loading,
        GetReady,
        Playing,
        Dead,
        Won
    }
}

Sedan ser vi till att använda oss av vårt meddelandesystem för att skicka uppdateringar/förfrågningar om att skifta tillstånd. Det är upp till spelet att prenumerera på dessa meddelanden och på så vis centralt på ett enda ställe hantera alla övergångar mellan olika tillstånd.

GameObjectManager

En stor del i själva städningen är att införa klassen GameObjectManager som kommer att ha hand om alla GameObject objekt (förutom Player). Denna klass är bara förflyttning av gammal kod. Här dyker också första exemplet på användandet av meddelandesystemet upp.

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()
        {
            ResetMeteors();

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

        public void ResetMeteors()
        {
            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 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);

            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);
            }
        }
    }
}

Videogenomgång

Ytterligare ändringar i kod

Som tidigare nämnts så är texten i artikeln här mest ett stöd för videogenomgången. Följ först videogenomgången sedan kan du jämföra din kod med den färdiga som listas under detta avsnitt.

Undvik klipp och klistra! Du lär dig mer av att skriva koden steg för steg när du följer videogenomgången.

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 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 int reloadTimer = 0;
        private Random rnd = new Random();

        public Player(Game game) : base(game)
        {
            Position = new Vector2(Globals.ScreenWidth / 2, Globals.ScreenHeight / 2);
        }

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

            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);
        }

        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 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
            };
        }
    }
}

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;

        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:
                    player.Enabled = gameObjectManager.Enabled = false;
                    break;
                case GameState.Playing:
                    player.Enabled = gameObjectManager.Enabled = true;
                    break;
                case GameState.Dead:
                case GameState.Won:
                    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");
        }

        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.GetReady:
                    if (kbState.IsKeyDown(Keys.Space) && previousKbState.IsKeyUp(Keys.Space))
                        Messenger.Instance.Send(new GameStateChangedMessage() { NewState = GameState.Playing });
                    break;
                case GameState.Playing:
                    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);

            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}

Lämna ett svar

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

Scroll to top