Converting a GameMaker: Studio project to MonoGame (Part 2)

In Part 1 we set up the solution for our game.

One thing I forgot to mention was that after importing room0.room.gmx, that too needs to have its Copy to Output Directory property set to “Copy if newer”.

Now let’s write some code.

TileBasedPlatformer class setup
Open TileBasedPlatformer.cs and add the following at the top of the class:

// the room which gets updated and drawn each frame.
Room currentRoom;

// for now, keep sprites/backgrounds as effectively globals
public static Texture2D Background;

public static Texture2D FallLeft;
public static Texture2D FallRight;
public static Texture2D JumpLeft;
public static Texture2D JumpRight;
public static Texture2D WalkLeft;
public static Texture2D WalkRight;

For a larger game, I would recommend some kind of resource manager class, but here we use the GM:S approach of using globals for the sprites. That is to say, any piece of code can reference the fall_left texture say by using TileBasedPlatformer.FallLeft.

We have also added an instance of our Room class to store the current room. We will implement this class later.

Now find the LoadContent method. Add the following:
FallLeft = Content.Load<Texture2D>("Sprites/fall_left");
FallRight = Content.Load<Texture2D>("Sprites/fall_right");
JumpLeft = Content.Load<Texture2D>("Sprites/jump_left");
JumpRight = Content.Load<Texture2D>("Sprites/jump_right");
WalkLeft = Content.Load<Texture2D>("Sprites/walk_left_strip8");
WalkRight = Content.Load<Texture2D>("Sprites/walk_right_strip8");

Background = Content.Load("Backgrounds/background2");

These lines all load texture files for our sprites and background. MonoGame does not need to be told the file extension, merely the subdirectory within Content where the file is stored, and it will infer the extension from the type (in this case Texture2D) loaded.

Next add:
currentRoom = new Room("room0");
This will hopefully do exactly what you expect it to, once we have written the method.

Tile struct
Now open up Room.cs.

The using directives we’ll need are as follows:
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Xml;
using System;
using System.Linq;

That’s a fairly extensive list, but I think it shows the power of MonoGame that we can use any part of the standard C# library we want.

Now, before the class Room, add the following struct:
struct Tile
{
  public Texture2D Background; // background used for drawing
  public int X; // x position (upper left) in room
  public int Y; // y position (upper left) in room
  public int W; // width in background
  public int H; // height in background
  public int XO; // position (upper left) in background
  public int YO; // position (upper left) in background
  public int ScaleX; // horizontal scale in room
  public int ScaleY; // vertical scale in room

  public Rectangle DestRect // rectangle in room in which tile is drawn
  {
   get { return new Rectangle(X, Y, W * ScaleX, H * ScaleY); }
  }

  public Rectangle SourceRect // rectangle in background from which tile is taken
  {
   get { return new Rectangle(XO, YO, W, H); }
  }
}

Hopefully this is fairly explanatory. Each instance of this struct comes from a line in the room xml file.
If you are not familiar with C# properties, the DestRect and SourceRect are like little parameterless functions which calculate things we will use for drawing.

Room class setup
GM:S stores rooms as a list of tiles and their positions, which is probably good for very sparse rooms, but in most cases we will want a lot of tiles, so I use a 2D array for each layer.

The collection for the tiles will be SortedList<int, Tile?[,]>. That’s a fairly complicated set of classes, so let’s break it down:
We have several layers of tiles, each at a different depth. This depth is an integer, and we want to be able to sort by this integer, so we used SortedList with int as the Key.
For the Value of the list, we have a 2D array. This represents the whole size of the room, where the size of the array will be [widthInTiles,heightInTiles] that you’ll see defined soon.
The array is made up of Tile?. That is, a nullable Tile. So at each entry in the array (i.e. position in the room) we can either have a Tile, or null.

We will also just use an ordinary List for our instances in the room.

So at the top of the Room class let’s add the following fields and properties:
int width; // width of the room in pixels
int height; // height of the room in pixels
int tileWidth; // normal (smallest) size of tiles
int tileHeight;

int widthInTiles // number of tiles in a row
{
  get { return width / tileWidth; }
}
int heightInTiles // number of tiles in a column
{
  get { return height / tileHeight; }
}

SortedList<int, Tile?[,]> layers; // array of tiles for each depth layer
List<Object> instances; // list of all object instances in room

Now add this method:

Tile?[,] GetOrAddLayer(int depth)
{
  if (layers.ContainsKey(depth))
  {
   // layer already exists, so return it
   return layers[depth];
  } else
  {
   // create the layer
   Tile?[,] layer = new Tile?[widthInTiles, heightInTiles];

   // set all tiles to null
   for(int i=0; i< widthInTiles; i++)
   {
    for(int j=0; j<heightInTiles; j++)
    {
     layer[i, j] = null;
    }
   }

   // add the layer to our sorted list
   layers.Add(depth, layer);

   // return the new layer
   return layer;
  }
}

This is a helper function, which either adds a new, empty layer at a given depth, or returns an existing one.

Room Loading
Let’s add the constructor we called earlier:

public Room(string name)
{
  LoadFromFile("Content/Rooms/" + name + ".room.gmx");
}

and so finally we can write the method which actually loads the room from xml.

void LoadFromFile(string fileName)
{
using (var reader = new XmlTextReader(fileName)) // open the file
{
  XmlDocument doc = new XmlDocument();
  doc.Load(reader); // load the file into memory

  XmlNode room = null;

  foreach(XmlNode child in doc.ChildNodes)
  {
   if(child.Name == "room")
   {
    room = child;
   }
  }

  if(room == null)
  {
   Console.WriteLine("room node not found in " + fileName);
  }

  // clear any current values
  width = 0;
  height = 0;
  tileWidth = 0;
  tileHeight = 0;
  layers = new SortedList<int, Tile?[,]>();
  instances = new List<Object>();

  // first go through children looking for basic room information
  foreach (XmlNode child in room.ChildNodes)
  {
   switch (child.Name)
   {
   case "width":
    width = Int32.Parse(child.InnerText);
    break;
   case "height":
    height = Int32.Parse(child.InnerText);
    break;
   case "vsnap":
    tileWidth = Int32.Parse(child.InnerText);
    break;
   case "hsnap":
    tileHeight = Int32.Parse(child.InnerText);
    break;
   } // could also inclue here e.g. speed (i.e. framerate)

   if (width != 0 && height != 0 && tileWidth != 0 && tileHeight != 0)
   {
    break; // have everything we need to set up room
   }
  }

  // now add object instances and tiles
  foreach (XmlNode child in room.ChildNodes)
  {
   if (child.Name == "instances")
   {
   // add each instance

   foreach(XmlNode instance in child.ChildNodes)
   {
    // get the object type and position
    var type = Type.GetType(instance.Attributes["objName"].Value);

    if (type != null)
    {

     int x = Int32.Parse(instance.Attributes["x"].Value);
     int y = Int32.Parse(instance.Attributes["y"].Value);

     // instantiate the thing and add it to our object list
     instances.Add((Object)Activator.CreateInstance(type, x, y)); // add the new instance
    } else
    {
     Console.WriteLine("Object " + instance.Attributes["objName"].Value + " not found"); // output a message, but do not throw
    }
   }
  }

  if(child.Name == "tiles")
  {
   // add each tile

   foreach(XmlNode tile in child.ChildNodes)
   {
    // create a new tile
    Tile newTile = new Tile();

    // fill its values from xml
    newTile.X = Int32.Parse(tile.Attributes["x"].Value);
    newTile.Y = Int32.Parse(tile.Attributes["y"].Value);
    newTile.W = Int32.Parse(tile.Attributes["w"].Value);
    newTile.H = Int32.Parse(tile.Attributes["h"].Value);
    newTile.XO = Int32.Parse(tile.Attributes["xo"].Value);
    newTile.YO = Int32.Parse(tile.Attributes["yo"].Value);
    newTile.ScaleX = Int32.Parse(tile.Attributes["scaleX"].Value);
    newTile.ScaleY = Int32.Parse(tile.Attributes["scaleY"].Value);

    // for now, just use the only background
    newTile.Background = TileBasedPlatformer.Background;

    int depth = Int32.Parse(tile.Attributes["depth"].Value);

    // add the tile to the correct array (or create one if none exists)
    GetOrAddLayer(depth)[newTile.X / tileWidth, newTile.Y / tileHeight] = newTile;
   }
  }
}
}
}

I don’t want to say too much about this method, since the aim of this series is not to teach xml in C#. There are plenty of resources out there to learn, but do feel free to email/tweet me with questions.

I will say though that this method ignores most of the structure of the GM:S room files, and just loads tiles and objects. It could easily be extended. It’s also not very robust xml, but should work with files generated by GM:S.

My game doesn’t use the GM:S room format for storing levels, nor indeed xml, but I thought this was necessary for the purpose of the tutorial. I would recommend writing your own format for tile data, and then hard-coding things like framerate and tile size.

The game should now run fine, and this is a good check on any typos. Nothing will appear different from before, though.

Drawing the tiles
We have now loaded all the tiles. Note that we haven’t loaded any object instances yet, since those have not been implemented, though the loading code is ready for when they are.

MonoGame in 2D draws with a class called SpriteBatch. The TileBasedPlatformer class contains a field called spriteBatch already in the template, we just need to use it.

In TileBasedPlatformer.cs, find the Draw method and add the following after GraphicsDevice.Clear(Color.CornflowerBlue);:
currentRoom.Draw(spriteBatch);
Clearly we now need to write this Room.Draw method.

Back in Room.cs, add:
public void Draw(SpriteBatch spriteBatch)
{
  spriteBatch.Begin();

  // iterate through layers (greatest depth first)
  foreach(var layer in layers.Values.Reverse())
  {
   // iterate over all tiles in layer
   for(int i =0; i<widthInTiles; i++)
   {
    for(int j=0; j<heightInTiles; j++)
    {
     if (layer[i, j] != null)
     {
      Tile tile = layer[i, j].Value;
      spriteBatch.Draw(tile.Background, tile.DestRect, tile.SourceRect, Color.White);
     }
    }
   }
  }

  spriteBatch.End();
}

First we call spriteBatch.Begin, which prepares MonoGame for rendering a group of sprites. This can be extended to include the setting up of blend states etc., but the form with no arguments is fine for now.

Then we iterate over all our tiles, and draw them. spriteBatch.Draw also has lots of different overloads. I’d point you to the MSDN documentation for any of these functions to understand exactly what each parameter does.

Finally we call spriteBatch.End, which tells MonoGame that we’re ready to draw all the sprites we’ve sent. It then draws them in the order in which we passed them.

That’s it! You should now get something that looks like this:
tiles

In the next part, we’ll implement our Object abstract class and translate the code for some of the objects.

One thought on “Converting a GameMaker: Studio project to MonoGame (Part 2)

Leave a Reply

Your email address will not be published. Required fields are marked *