Inledning
Denna gång var det dags att kika på hur vi kan spara data i XML med de vertyg som finns i XNA. Som exempel kommer vi att hantera animationsdata till olika karaktärer, något som är ganska vanligt i spel. Vi har i äldre artiklar tittat på Animationer och Serialisering av data till XML . Denna gång kommer vi att presentera ett användbart exempel. Målet är att beskriva animeringar via XML och utifrån en "sprite sheet" (bild med 2D animationer) kunna ladda in olika animationer.
Nu skiljer sig den XML-serialisering som finns i XNA-ramverket mot den som vi kanske vanligtvis använder för windowsapplikationer. Serlialiseraren vi skall använda idag heter ContentSerializer och är en del av XNA-ramverket (Microsoft.Xna.Framework.Content).
Det finns många artiklar på nätet som argumenterar för eller emot användandet av den "vanliga" XML-serialiseringen (XmlSerializer) i XNA-spel. Se inte denna artikel som varken en rekommendation eller ett ställningstagande utan som en möjlighet att lära mer om den XML-hantering som finns i XNA från början. Glöm inte bort att XNA omfattar både Windows, XBox 360 och Windows Phone mobiler, därför kan det vara en poäng att använda XML-hanteringen i XNA då denna ska fungera på alla plattformar.
Datan
Vi börjar med att göra en modell över den data vi vill representera. Målet är att vi skall kunna ladda objekt utifrån data specificerade i XML-filer.
Vi tänker oss en design där Character innehåller namn (Name), position på skärmen (Position), animationer (Animations) samt logik som sköter uppdatering (Update) och uppritning (Draw). Till detta behöver vi en klass Animation som beskriver en animation. Animationen innehåller namn (Name), texturhänsvisning (Asset), bildrutor (Frames) och aktuell bildruta (CurrentFrame) samt logik för uppdatering (Update) och nollställning av animation (Reset). Sist men inte minst behöver vi en klass Frame. Denna innehåller en rektangel (Rectangle) som beskriver vilken del av texturen (Asset i animationsklassen) som grafiken skall hämtas från samt hur lång tid bilden skall visas (Duration).
Dataklasserna måste placeras i ett separat projekt av typen "Windows Game Library". Detta för att både spelet och vår "Content" måste känna till klasserna. Gör vi inte detta så kommer XML-serialiseringen inte att fungera då "Content" inte kan referera till vårt spel utan att det blir ett cirkulärt beroende. Mer om detta senare. Vi kallar vårt "Windows Game Library" för "DataModel",
Nu tar vi en titt på klasserna.
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
namespace DataModel
{
public class Frame
{
public TimeSpan Duration { get; set; }
public Rectangle Rectangle { get; set; }
[ContentSerializerIgnore]
public Vector2 Center
{
get
{
return new Vector2(this.Rectangle.Width / 2.0f, this.Rectangle.Height / 2.0f);
}
}
}
}
På egenskapen Center har vi lagt till attributet ContentSerializerIgnore för att utesluta denna egenskap från serialiseringsprocessen. Detta då Center är en beräknad egenskap utifrån data som vi kommer att spara ändå.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework;
namespace DataModel
{
public class Animation
{
public string Name { get; set; }
public string Asset { get; set; }
public List<Frame> Frames { get; set; }
[ContentSerializerIgnore]
public Frame CurrentFrame { get { return Frames[frameIndex]; } }
private TimeSpan time;
private int frameIndex = 0;
public Animation()
{
Frames = new List<Frame>();
}
public void Reset()
{
time = TimeSpan.Zero;
frameIndex = 0;
}
public void Update(GameTime gameTime)
{
time += gameTime.ElapsedGameTime;
if (time > CurrentFrame.Duration)
{
frameIndex++;
if (frameIndex >= Frames.Count)
frameIndex = 0;
time = TimeSpan.Zero;
}
}
}
}
Vi har en vanlig lista av typen Frame. I uppdateringen mäter vi tiden som passerat sedan förra uppdateringen och undersöker om vi behöver byta frame. Når vi slutet av animationen så börjar vi automatiskt om. CurrentFrame väljer vi att inte spara, återigen med attributet ContentSerializerIgnore.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
namespace DataModel
{
public class Character
{
public string Name { get; set; }
public Vector2 Position { get; set; }
public List<Animation> Animations { get; set; }
[ContentSerializerIgnore]
public Animation CurrentAnimation { get { return Animations[animationIndex]; } }
[ContentSerializerIgnore]
public Dictionary<string, Texture2D> Textures { get; set; }
int animationIndex = 0;
public Character()
{
Animations = new List<Animation>();
Position = new Vector2(300, 400);
}
public void NextAnim()
{
animationIndex++;
if (animationIndex >= Animations.Count)
animationIndex = 0;
}
protected void StartAnimation(string name)
{
if (name == CurrentAnimation.Name)
return;
for(int i=0; i<Animations.Count; i++)
{
if (Animations[i].Name == name)
{
animationIndex = i;
CurrentAnimation.Reset();
}
}
}
public void Update(GameTime gameTime)
{
CurrentAnimation.Update(gameTime);
}
public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
spriteBatch.Draw(Textures[CurrentAnimation.Asset], Position,
CurrentAnimation.CurrentFrame.Rectangle, Color.White);
}
}
}
Grafik
Vi måste ju ha lite grafik att testa med innan vi sätter samman allt. Vi ha valt lite grafik från opengameart.org som föreställer en kanin. Denna grafik ligger med i projektet som du kan ladda hem i slutet av artikeln.
Serialisering
Nu kommer vi till det viktiga. Det finns vissa saker som måste uppfyllas.
Lägg till referenser
Både "Content"-delen i vårt spel och själva spelet måste känna till projektet "DataModel" som vi skapade i ett separat "Windows Game Library"-projekt. Vi navigerar till "References" och väljer att lägga till. Se bild.
Välj fliken "Projects", välj projektet "DataModel" och sist "OK".
Skapa en XML-fil i Content
Högerklicka i "Content"-projekt och välj lägg till. Välj sedan "XML" i listan. Vi valde att kalla XML-filen för rabbit_data.
Regler för XML-filen
Några snabba regler. Vi återkommer till dessa efter XML-filen.
- Asset i XML-filen måste definieras som klassens fullständiga "Assembly-name"
- Klassen måste vara publik
- Endast public data (egenskaper och variabler) sparas/laddas, dvs. ej private, protected och internal
- Ordningen i XML-filen är viktig. Först listas egenskaper i den ordning de står i kod-filen därefter eventuella publika variabler
- Egenskaper med bara get- eller bara set-delar ignoreras
- Allt skrivs som element. Du kan inte styra viss data som attribut
<?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
<Asset Type="DataModel.Character">
<Name>Rabbit</Name>
<Position>300 200</Position>
<Animations>
<Item>
<Name>Idle</Name>
<Asset>rabbit</Asset>
<Frames>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>9 19 45 73</Rectangle>
</Item>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>54 19 45 73</Rectangle>
</Item>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>99 19 45 73</Rectangle>
</Item>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>144 19 45 73</Rectangle>
</Item>
</Frames>
</Item>
<Item>
<Name>Crouching</Name>
<Asset>rabbit</Asset>
<Frames>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>14 273 40 73</Rectangle>
</Item>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>64 273 40 73</Rectangle>
</Item>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>108 273 40 73</Rectangle>
</Item>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>147 273 40 73</Rectangle>
</Item>
</Frames>
</Item>
<Item>
<Name>Ball</Name>
<Asset>rabbit</Asset>
<Frames>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>185 274 34 73</Rectangle>
</Item>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>217 274 34 73</Rectangle>
</Item>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>251 274 34 73</Rectangle>
</Item>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>283 274 34 73</Rectangle>
</Item>
</Frames>
</Item>
<Item>
<Name>Punch</Name>
<Asset>rabbit</Asset>
<Frames>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>10 447 40 73</Rectangle>
</Item>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>50 447 59 73</Rectangle>
</Item>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>114 448 40 73</Rectangle>
</Item>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>158 448 40 73</Rectangle>
</Item>
<Item>
<Duration>PT0.2S</Duration>
<Rectangle>200 448 55 73</Rectangle>
</Item>
</Frames>
</Item>
</Animations>
</Asset>
</XnaContent>
Några slutsatser vi kan dra från exemplet ovan:
- Vector2 skrivs som x y
- Rectangle skrivs som x y width height
- TimeSpan skrivs som PT0.2S där 0.2 i detta exempel är 200ms
- Variabler som bygger på lagringsklasser, t.ex. List skrivs som underliggande element där varje data ligger som en
XML-filen passerar "Content Pipeline" och blir kompilerad till en "rabbit_data.xnb" tillsammans med all annan "content" man kan tänkas ha. Detta kan vara en fördel då den vanliga användaren får svårt att redigera filen så på så viss får den ett enkelt dataskydd. Nackdelen är att XML-filen måste kompileras om när ändringar görs.
Formatet på t.ex. TimeSpan var nära omöjligt att hitta på nätet. Dålig dokumentation får vi nog lägga till egenskaperna för XML i XNA. Övriga datatyper som Color, Matrix etc. finns det exempel på internet likaså array'er av int och liknande. Google är din vän.
Inladdning av data
Nu komemr den enkla biten, att ladda in data. I vår LoadContent kan vi nu skriva:
//...
Dictionary<string, Texture2D> textures;
KeyboardState prevState;
Character Rabbit;
SpriteFont font;
//..
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>("SpriteFont1");
Rabbit = Content.Load<Character>("rabbit_data");
Rabbit.Textures = textures;
foreach (Animation a in Rabbit.Animations)
{
if (!textures.ContainsKey(a.Asset))
textures.Add(a.Asset, Content.Load<Texture2D>(a.Asset));
}
}
I exemplet så hanterar vi texturerna som behövs lite speciellt. Om ni minns så hade varje animation en Asset som var namnet på en Texture2D content. Vi måste någon gång ladda in de texturer som animationerna använder. Det finns andra sätt att lösa detta på men artikelns mål var att belysa XML i XNA.
Ett exempel
Vi har samplat kod, grafik etc. i ett färdigt projekt för dig att ladda hem. Vi har lagt in lite logik för att uppdatera karaktären och själva uppritningen. Genom att rycka på N kan vi växla mellan animationerna.
protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
KeyboardState state = Keyboard.GetState();
if (prevState.IsKeyUp(Keys.N) && state.IsKeyDown(Keys.N))
Rabbit.NextAnim();
Rabbit.Update(gameTime);
prevState = state;
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
//Info
spriteBatch.DrawString(font, "Animation: " +
Rabbit.CurrentAnimation.Name, new Vector2(10, 10), Color.White);
Rabbit.Draw(gameTime, spriteBatch);
spriteBatch.End();
base.Draw(gameTime);
}
Avslutning
Följande fördelar kan vi konstatera med XML-hanteringen i XNA:
- Skall stödjas av Windows, Xbox 360, Windows Phone då det är en del av XNA-ramverket
- Ger ett visst dataskydd då XML-filerna passerar "Content Pipeline" och blir .xnb-filer
- Serialiserar vissa datatyper som inte stöds i XmlSerializer, t.ex. TimeSpan och Rectangle m.fl.
- Alla övriga fördelar som XML har i projekt, dvs. separering mellan data och kod.
Det finns givetvis en del nackdelar, då jämfört med XmlSerializer
- Allt blir element! XML-filen blir stor
- Måste kompilera om ändringar i XML-filen, inget stort bekymmer
- Dokumentationen på nätet är dålig