Multiplayer Tank – Del 2

Grafik

Till vårt projekt behövs lite schysst grafik. Till en början behövs grafik till 2 Tanks, ett skott, någon form av mätare, en enkel bana samt en animerad explosion. För att bättre illustrera grafiken så har vi här förstorat den. Nedanför hittar ni en länk där ni kan ladda hem grafiken.

bild

bild

Implementation - steg ett

Vi kommer nu som hastigast att skapa ett fungerande spel som om det vore ett spel utan möjlighet till nätverksspel för att i ett senare skede lägga till den funktionen. Vi kommer enbart att titta närmare på sådant som ej behandlats i tidigare artiklar vilket gör att det går lite snabbt fram ifall man aldrig jobbat med OOP eller XNA. Kollisionshantering kommer i del 3 och därefter nätverksstödet.

Vi börjar med att skapa ett projekt som vi döper till MultiPlayerTanks. Vi packar upp grafiken i content-mappen till vårt projekt och lägger till grafiken i vår content-manager.

bild

Vi har valt att använda en enkel mark-textur som grafik till vår "bana". Själva banan kan hanteras på ett betydligt bättre sätt, exempelvis skapa ett tile-baserat system för grafik och speciella föremål/hinder på banan. I detta fall nöjer vi oss dock med en enkel (relativt stor) textur vars enda syfte är att ge en känsla av rörelse vid förflyttning.

Vi börjar med att skapa de olika klasserna. Exempelvis genom att högerklicka på vårt projekt i content-manager och välja Add - Class..

bild

Vi börjar med att skapa klassen GameObj.

Klassen GameObj

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace MultiPlayerTanks
{
    class GameObj
    {
        public Vector2 Position //Objektets position
        {
            get;
            set;
        }
        public Texture2D Gfx //Objektets grafik
        {
            get;
            set;
        }
        public float Angle //Objektets vinkel
        {
            get;
            set;
        }
        public virtual void Draw(SpriteBatch spriteBatch, Vector2 DrawOffset)
        {
            spriteBatch.Draw(Gfx, 
				Position - DrawOffset + new Vector2(400, 300), null, 
				Color.White, Angle + (float)Math.PI / 2,
                new Vector2(Gfx.Width / 2, Gfx.Height / 2), 1.0f, 
				SpriteEffects.None, 0);
        }
    }
}

Notera att vi använder något som kallas för Property (plural properties) som introducerades med C# 3.0 och är ett extremt smidigt och flexibelt sätt att läsa, skriva och beräkna värdet för privata medlemmar (eng. fields) (en medlem är en klassvariabel). En property är egentligen en form av metod som kallas accessor. Alla som sysslat lite med OOP vet att det blir en himla massa set- och get-metoder för att läsa, ändra och beräkna privata medlemmar. Detta blir alltså betydligt lättare och mer överskådligt med properties. Mer information hittar ni på webbplatsen för MSDN

De properties som alla objekt har är Position, Gfx och Angle. Sedan lägger vi till en Draw-metod så att objektet kan rita ut sig på skärmen. Draw-metoden tar 2 in-parametrar. Dels en SpriteBatch som vi skickar med när vi anropar metoden så att objektet kan rita ut sig själv och dels en Vector2 som används för att korrigera var objektet ritas ut i förhållande till vår spelare .

Alla objekt kommer att ha en "riktig" världsposition men när vi ritar ut grafiken så vill vi att vår spelare skall vara centrerad i mitten på skärmen hela tiden. Detta gör att vi behöver rita ut vissa objekt med en viss förskjutning (offset) som kompenserar för detta. Dock skall vissa objekt som mätare och text ritas ut på normalt vis. En alternativ lösning på detta problemet är att flytta "kameran" (viewport) som bestämmer vad som skall visas (lite enkelt uttryckt). Eller använda den omvandlingsmatris som finns. Det i min mening enklaste sättet för att åstadkomma detta i vårt projekt är att skicka en vektor till utritningsmetoden som korrigerar detta.

Själva utritningen sker med ett anrop till spriteBatch.Draw som tar 10 inparametrar och som beskrivits i projektet Asteroids här på csharpskolan. Vi använder bl a objektets medlem Angle för att kunna rotera grafiken som skall ritas ut samt centrera den.

Klassen MovingGameObj

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace MultiPlayerTanks
{
    class MovingGameObj : GameObj
    {
        public Vector2 Direction //Riktning 
        {
            get;
            set;
        }
        public float Speed //Hastighet
        {
            get;
            set;
        }

        public virtual void Update(GameTime gameTime)
        {
            Position += Direction * Speed;
        }
    }
}

Klassen MovingGameObj används för alla objekt som skall kunna röra på sig och ärver från klassen GameObj. Vi har properties för hastiget, Speed och riktning, Direction. Vi kommer än så länge att använda direction för riktning (som en normerad vektor med längden 1) och tillsammans med hastighetskonstanten (speed) bestämma förflyttningen av objektet.

Klassen Tank

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace MultiPlayerTanks
{
    class Tank : MovingGameObj
    {
        public Tank()
        {
            MaxSpeed = 2.5F;
            ShotPower = 0;
            prevKs = Keyboard.GetState();
            Life = 100F;
            Kills = 0;
            Angle = -(float)(Math.PI/2);
        }
        public bool Enemy
        {
            get;
            set;
        }
        public float MaxSpeed
        {
            get;
            set;
        }
        public float ShotPower
        {
            get;
            set;
        }
        public int WeaponType
        {
            get;
            set;
        }
        public bool ShotFired
        {
            get;
            set;
        }
        public float Life
        {
            get;
            set;
        }
        public int Kills
        {
            get;
            set;
        }
        protected KeyboardState prevKs;


        public void Respawn()
        {
            Life = 100F;
            Random randomerare = new Random();
            Position = new Vector2(randomerare.Next(1000), randomerare.Next(1000));
            Angle = 0;

        }
        public override void Update(GameTime gameTime)
        {

            KeyboardState ks = Keyboard.GetState();

            if (ks.IsKeyDown(Keys.Up))
            {
                if (Speed < 0) Speed = 0;
                if (Speed < MaxSpeed) Speed = Speed * 1.005F + 0.01F;
                else Speed = MaxSpeed;
            }
            if (ks.IsKeyDown(Keys.Down))
            {
                if (Speed > -1.0F) Speed -= 0.04F;
                else Speed = -1.0F;
            }
            if (ks.IsKeyUp(Keys.Down) && ks.IsKeyUp(Keys.Up) && Speed> 0)
            {
                Speed -= 0.01F;
                if (Speed <= 0) Speed = 0;
            }
            if (ks.IsKeyUp(Keys.Down) && ks.IsKeyUp(Keys.Up) && Speed < 0)
            {
                Speed += 0.01F;
                if (Speed >= 0) Speed = 0;
            }

            if (ks.IsKeyUp(Keys.Left))
            {
                Angle += 0.02F;
            }
            if (ks.IsKeyUp(Keys.Right))
            {
                Angle -= 0.02F;
            }
            if (ks.IsKeyDown(Keys.Space))
            {
                if (ShotPower < 100) 
                    ShotPower += 0.5F;
                else 
                    ShotPower = 100;
            }

            if (ks.IsKeyUp(Keys.Space) && prevKs.IsKeyDown(Keys.Space))
            {
                //ShotPower = 0;
                ShotFired = true;
            }

            prevKs = ks;
            Direction = new Vector2((float)Math.Cos(Angle), (float)Math.Sin(Angle));

            base.Update(gameTime);
        }
    }
}

De flesta medlemmar och metoder till klassen Tank är ganska självklara men vi kommer förtydliga dem och dess funktion för säkerhetsskull. Klassen ärver från klassen MovingGameObj.

Tank() Konstruktorn för klassen används för att tilldela vissa medlemsvariabler och ett startvärde
Enemy Denna bool används för att avgöra vilka Tanks som är fiende och vilken tank som är vår spelares. Detta har betydelse vid utritning av tanksen.
MaxSpeed Används för att bestämma maxhastigheten för förflyttningen.
ShotPower Används för att bestämma hur långt skottet ska förflytta sig när man avfyrat ett skott
WeaponType Används inte i nuläget men kommer antagligen att behövas ifall vi vil kunna byta vapen i framtiden. Variabeltypen enum lämpar sig eventuellt bättre.
ShotFired Denna bool används för att bestämma ifall ett skott har avfyrats (ska avfyras)
Life Lagrar livet för vår tank
Kills Lagrar antalet dödade tanks som vår spelare/tank har på sitt samvete
prevKS Detta är en medlemsvariabel av typen protected och ingen property eftersom den enbart används inom klassen. Används för att hålla koll på föregående tangentbordsstatus
Respawn() Denna metod används för att återställa en tank, exempelvis när en tank har "dött". Vi slumpar ut en ny position och återställer livet mm.
Update() I denna metod samlar vi all uppdateringslogik för vår tank. Som inparameter skickar vi gameTime som är den förflutna tiden i spelet vilket vi eventuellt behöver använda. Vi läser av tangentbordet och gör enkla algoritmer för att accelerera och bromsa vår tank. Vi använder piltangenterna för att svänga och gasa/bromsa. För att skjuta används SPACE-tangenten och vi när vi håller den inne så "laddas" vår ShotPower och skottet avlossas först när vi släpper SPACE-tangenten igen. Sist men inte minst så uppdateras positionen.
Klassen Shot

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace MultiPlayerTanks
{
    class Shot : MovingGameObj
    {
        public float Power
        {
            get;
            set;
        }

        public override void Update(GameTime gameTime)
        {
            Power -= 1.1F;
            base.Update(gameTime);
        }
    }
}

Klassen Shot ärver från MovingGameObj och är ganska enkel. Vi har en property Power som används för att avgöra hur långt skottet ska flyga. Metoden Update() används för att uppdatera skottet. I detta fall minska Power och ändra Positionen.

Klassen Meter

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace MultiPlayerTanks
{
    class Meter : GameObj
    {
        public Meter()
        {
            Value = 0;
        }
        public float Value
        {
            set;
            get;
        }
        public override void Draw(SpriteBatch spriteBatch, Vector2 DrawOffset)
        {
            spriteBatch.Draw(Gfx, new Rectangle((int)base.Position.X, 
                (int)base.Position.Y, 
                (int)this.Value, Gfx.Height), Color.White);
        }
    }
}

Klassen Meter ärver från klassen GameObj eftersom mätaren inte behöver flytta på sig. Konstruktorn Meter() används för att sätta värdet på property'n Value till 0. Value är värdet på det som mätaren skall visa och används i metoden Draw() som används istället för den ärvda metoden från GameObj i och med override. För att rita ut (grafiken till) mätaren så används en Rectangle vars startposition (övre vänstra hörn) är detsamma som mätarens position och "slutposition" (nedre högra hörnet) är höjden av grafikens höjd och bredden är Value. Detta gör att vår mätare skalas beroende på värdet. En Rectangle anges med heltal därför måste vi typkonvertera (eng. typecast) till int där så behövs.

Klassen Explosion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace MultiPlayerTanks
{
    class Explosion : GameObj
    {
        public Explosion()
        {
            Time = 0;
            Frame = 0;
            Active = true;
            AnimationSpeed = 30;
            Angle = 0;
        }

        public int Time
        {
            get;
            set;
        }
        public int Frame
        {
            get;
            set;
        }
        public int AnimationSpeed
        {
            get;
            set;
        }
        public bool Active
        {
            get;
            set;
        }
        public void Update(GameTime gameTime)
        {
            Time += gameTime.ElapsedGameTime.Milliseconds;
            if (Time > AnimationSpeed)
            {
                Time = 0;
                Frame++;
                if (Frame > 15)
                {
                    Active = false;
                }
            }
        }

        public override void Draw(SpriteBatch spriteBatch, Vector2 DrawOffset)
        {
            Rectangle tmp = new Rectangle((Frame % 4) * 64, (Frame / 4) * 64, 64, 64);

            spriteBatch.Draw(Gfx, 
                Position - DrawOffset + new Vector2(400, 300), 
                tmp, Color.White, base.Angle,
                new Vector2(32, 32), 1.0f, SpriteEffects.None, 0);
        }
    }
}

Klassen Explosion används för animerade explosioner (hör och häpna). Konstruktorn Explosion() används för att ge viktiga medlemmar ett värde när en ny instans av klassen Explosion skapas. Hela klassen och animeringen av explosionen fungerar på liknande sätt som de animationer som beskrivits i tidigare artiklar (exempelvis denna.

Time Förfluten tid i milliesekunder
Frame Aktuell bildruta
AnimationSpeed Fördröjningen (i ms) mellan varje bildruta
Active Används för att bestämma ifall explosionen är aktiv eller inte
Update() Denna metod uppdaterar explosionen genom att kolla hur mycket tid som förflutit sedan förra uppdateringen och ändra bildruta ifall mer tid än AnimationSpeed har förflutit. Efter 16 bildrutor så sätts Ative till false
Draw() Denna metoden skriver över den ärvda Draw-metoden och används för att rita ut rätt bildruta från grafikfilen för explosionen (där alla bildrutor är i samma bild). Precis som i tidigare exempel på animationer så är bilden som innehåller bildrutorna 256x256 pixlar och delas in i 16 olika bildrutor.

Lägg till en SpriteFont

Vi vill kunna skriva ut lite saker på skärmen och därför måste vi lägga till en spritefont (som beskrivits i tidigare artiklar).

bild

Enda skillnaden nu är att vi ändrar lite i inställningarna för fonten (xml-filen SpriteFont1.sritefont). Vi ändrar style från Regular till Bold.

bild

Game1.cs

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;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace MultiPlayerTanks
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        Tank Player1, Player2; //skapa två spelare/tanks
        GameObj Bana; //Skapa ett GameObj som ska användas som bana
        SpriteFont font; //Spritefont för utskrift
        Meter PowerMeter, Player1LifeMeter, Player2LifeMeter; //Mätare för liv och kraft

        List<Shot> allShots = new List<Shot>(); //Ny lista för alla skott(-objekt)
        Texture2D shot1Gfx; //Grafik till skotten

        List<Explosion> allExplosions = new List<Explosion>(); //Ny lista för alla explosioner
        Texture2D explosionGfx; //Grafik till explosionen
        string displayMessage = ""; //Text som skall skrivas ut

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        protected override void Initialize()
        {
            base.Initialize();
        }

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            
            //Ladar in grafiken
            Texture2D greenTankGfx = Content.Load<Texture2D>("green_tank");
            Texture2D redTankGfx = Content.Load<Texture2D>("red_tank");
            Texture2D level1Gfx = Content.Load<Texture2D>("level");
            Texture2D redMeterGfx = Content.Load<Texture2D>("meter");
            Texture2D lifeMeterGfx = Content.Load<Texture2D>("life_meter");
            shot1Gfx = Content.Load<Texture2D>("shot");
            explosionGfx = Content.Load<Texture2D>("explosion");

            //Skapa Objekt
            //Tilldelning av properties direkt.. slipper skriva konstruktorer
            Player1 = new Tank() 
            { 
                Gfx = greenTankGfx, Position = new Vector2(400, 300), 
                Speed = 0, Direction = new Vector2(0, -1), Enemy = false 
            };
            Player2 = new Tank() 
            { 
                Gfx = redTankGfx, Position = new Vector2(500, 400), 
                Speed = 0, Direction = new Vector2(0, -1), Enemy = true 
            };
            Bana = new GameObj() { Gfx = level1Gfx, Position = new Vector2(0, 0), Angle = 0 };
            PowerMeter = new Meter() { Gfx = redMeterGfx, Position = new Vector2(350, 350) };
            Player1LifeMeter = new Meter() { Gfx = lifeMeterGfx, Position = new Vector2(50, 50) };
            Player2LifeMeter = new Meter() { Gfx = lifeMeterGfx, Position = new Vector2(250, 50) };
            font = Content.Load<SpriteFont>("SpriteFont1");
        }

        protected override void UnloadContent()
        {
        }

        protected override void Update(GameTime gameTime)
        {
            Player1.Update(gameTime);   //Uppdatera spelare1 
            //Sätter Kraftmätarens värde till vår spelares "skottkraft"
            PowerMeter.Value = Player1.ShotPower;
            //Sätter Livmätarnas värde till resp. spelares liv-värde
            Player1LifeMeter.Value = Player1.Life; 
            Player2LifeMeter.Value = Player2.Life;

            if (Player1.ShotFired) //Ifall ett skott avfyrats
            {
                //Lägger till ett nytt skott i listan
                allShots.Add(new Shot() 
                { 
                    Gfx = shot1Gfx, 
                    Position = Player1.Position + (Player1.Direction * (Player1.Gfx.Height) / 2) + 
                                (Player1.Direction * shot1Gfx.Height / 2), 
                    Angle = Player1.Angle, 
                    Speed = 5.0F + Player1.Speed, 
                    Power = Player1.ShotPower, 
                    Direction = Player1.Direction 
                });
                Player1.ShotPower = 0;
                Player1.ShotFired = false;
            }
            for (int i = 0; i < allShots.Count; i++) //Loopar igenom alla skott
            {
                allShots[i].Update(gameTime); //uppdaterar skott
                if (allShots[i].Power < 0) //Är skottets "kraft" slut?
                {
                    //Lägg till en ny Explosion
                    allExplosions.Add(new Explosion()
                        { Gfx = explosionGfx, Position = allShots[i].Position });					
                    allShots.RemoveAt(i); //Tar bort skottet
                }
            }
            for (int i = 0; i < allExplosions.Count; i++) //Loopa igenom alla explosioner
            {
                allExplosions[i].Update(gameTime); //Uppdatera explosion
                //Ta bort "färdiga" explosioner
                if (allExplosions[i].Active == false) allExplosions.RemoveAt(i);
            }

            base.Update(gameTime);
        }

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

            spriteBatch.Begin();
            //Ritar ut bana, spelare och mätare
            Bana.Draw(spriteBatch, Player1.Position);
            Player1.Draw(spriteBatch, Player1.Position);
            PowerMeter.Draw(spriteBatch, Player1.Position);
            Player2.Draw(spriteBatch, Player1.Position);
            Player1LifeMeter.Draw(spriteBatch, new Vector2(0, 0));
            Player2LifeMeter.Draw(spriteBatch, new Vector2(0, 0));

			//Loopar igenom alla skott och ritar ut dem
            for (int i = 0; i < allShots.Count; i++) 
            {
                allShots[i].Draw(spriteBatch, Player1.Position);
            }
			//Loopar igenom alla explosioner och ritar ut dem
            for (int i = 0; i < allExplosions.Count; i++)
            {
                allExplosions[i].Draw(spriteBatch, Player1.Position);
            }
            //Skriver ut spelarnas namn och liv mm..
            string nameFormat = "{0}\nLife: {1}%\n\nKills: {2}";
            displayMessage = string.Format(nameFormat, "Player1", Player1.Life, Player1.Kills);
            spriteBatch.DrawString(font, displayMessage, new Vector2(51, 4), Color.Black);
            spriteBatch.DrawString(font, displayMessage, new Vector2(50, 5), Color.White);
            displayMessage = string.Format(nameFormat, "Player2", Player2.Life, Player2.Kills);
            spriteBatch.DrawString(font, displayMessage , new Vector2(251, 4), Color.Black);
            spriteBatch.DrawString(font, displayMessage, new Vector2(250, 5), Color.White);
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}

Game1.cs bör inte innehålla några lustigheter. Det som kan vara värt att kommentera är avsaknaden av inparametrar till konstruktorn för de olika objekten när nya instanser av klasserna skapas. Istället så anges värden på propertis direkt inom {}-parenteserna.

Vi skapar ett enkelt spel utan några "gamestates" och än så länge ingen kollisionshantering (kommer i del 3). För att få en enkel skugg-effekt på utskriften så skrivs varje text ut två gånger. Först i svart och sedan något förskjutet i vitt och på så sätt åstadkommer man den effekten. Alla objekt (utom mätarna) ritas ut i förhållande till vår spelares tank eftersom den skall vara centrerad.

Vår bana är en enkel GameObj som bara ritas ut. Ska vi göra något av banan så bör vi i ett senare skede skapa en egen klass för banor. Dessa kan med fördel vara tile-baserade för att förenkla ban-byggandet och optimera det hela lite.

När allt är klart bör vi ha något i stil med detta (se bild nedanför).

bild

Lämna ett svar

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

Scroll to top