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.
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.
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..
Vi börjar med att skapa 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 .
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.
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.
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. |
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.
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.
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).
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.
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.
När allt är klart bör vi ha något i stil med detta (se bild nedanför).