Difference between revisions of "Simple RPG"
(→Player Bounds) |
(→Collectables) |
||
Line 328: | Line 328: | ||
==Collectables== | ==Collectables== | ||
+ | Load your map again in Tiled, this time create a new object layer called Collectables. Then within this layer i have created 3 new objects called Coin_1 , Coin_2 , and Coin_3. I have also set the type to Coin and the height & width of each object to 16 pixels: | ||
+ | |||
+ | [[File:Collectables.gif]] | ||
+ | |||
+ | Now, click on the Collectables layer and in the properties panel add a custom property (+ symbol in bottom left corner). Make an integer called Coin_Count: | ||
+ | |||
+ | [[File:Coin Count.gif]] |
Revision as of 08:38, 19 November 2017
This tutorial will create a Tiled map based RPG game, which uses a collision layer within your map to control where the player can go. It will also show you how to create an objects layer for the player, another character, and also collectables.
Contents
Creating the Map
Tiled
You will firstly need to install the Tiled program from the website and link below. In college the Tiled executeables are on moodle, under project, technical skill, monogame, and tiled. I have also added links to other tutorials for using Tiled.
Tiled Website and Download
Tutorials for using Tiled
Written Version of Above Tutorials
Create a Map in Tiled
Map Settings
You will need to create a new map in tiled, the settings window below should be displayed:
The Tile size will need to match the tile size of your tileset. You can also specify the number of tiles in your your map, this and the tile size will create a map of a given size in pixels. You should be able to leave everything else the same.
You should now have an empty map, later on we will use Square.Tiled to draw the map this only supports maps in the Base64 GZIP format. So in the properties panel set the Tile Layer Format to Base64 (gzip compressed):
File:Tiled compression setting.gif
Tileset
Now to import the tileset, i'm using one of the 16x16 dungeon tileset on the project page of the computer science moodle page. If you are using a different tileset you may need to set a different tile height and width. To import a tileset you need to click the New Tileset icon in the bottom right corner:
Now the new tileset panel will appear:
Your tileset can be individual images, it is more usual for them to be on a single tileset image. You must click the embed in map option, Square.Tiled doesn't support external tilesets (TSX files). Some tileset images might also use a margin or spacing between each tile, this screen will allow you to set these if needed.
Map Layers
Your map should already have a layer called Map Layer 1, use the new layer button to also add another tile layer and then an object layer:
You can click a layer and the rename the layer in the properties panel, i will rename my bottom layer collision, my middle layer dungeon, and my object layer objects. When you are using layers you must always check which layer is currently in use, because it is quite common to add things into the wrong layer.
So with the dungeon layer selected (i have also hidden the other layers) use the tiles to draw a room. I have used the bricks tile to create the walls and then flood filed the floor texture:
Now unhide the collision layer, and make sure it is selected. Now choose a tile and follow the walls in the dungeon layer:
So if any part of your map needs to be inaccessible by the player, make sure the tiles on the collision layer zone off the area.
We now need to set the player object, this will allow us to load a texture onto the object, and move the object. So select the object layer and then the new rectangle tool:
Now click the starting position for your playing character on the map, this will place a rectangle on the screen and in the properties panel you should set the name of the object. You also need to make sure you give it a height and width. I have named mine player:
So now we will save this map, we will add things to this map later.
MonoGame Project
Create a new MonoGame project, mine is a Windows project.
Setup Square.Tiled
If you have a project ready, create a new class in your project. Click project and new class and call it Tiled.cs, then copy the code from this document over the code in your new class: Square.Tiled Class
Remember to set the name space to the correct namespace for your project.
You will need to add references in the using section for the following:
using System.IO;
using Squared.Tiled;
Code to Display Map
Map Variables
At the top of your Game1 class add these additional variables:
Map map;
Layer collision;
Vector2 viewportPosition;
int tilepixel;
LoadContent for map
In the LoadContent method add the following lines to load the map, the collision layer and to set the texture of the player. The variable tilepixel assumes your tiles are square, the number of pixels is taken from the map:
map = Map.Load(Path.Combine(Content.RootDirectory, "SimpleRPG.tmx"), Content);
collision = map.Layers["Collision"];
tilepixel = map.TileWidth;
map.ObjectGroups["objects"].Objects["Player"].Texture = Content.Load<Texture2D>("hero");
The Update Method
You will also need to update the viewportPosition, this will center the map onto the player:
viewportPosition= new Vector2(map.ObjectGroups["objects"].Objects["Player"].X - (graphics.PreferredBackBufferWidth/2), map.ObjectGroups["objects"].Objects["Player"].Y - (graphics.PreferredBackBufferHeight/2));
The Draw Method
Add the following to the draw method to draw the map and hero to the screen.
If you already have spriteBatch.Begin() or spriteBatch.End() then just place the middle line inbetween your lines.
spriteBatch.Begin();
map.Draw(spriteBatch, new Rectangle(0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height), viewportPosition);
spriteBatch.End();
At this point your project should run an display your map centered onto the player.
Move Player
Create a new method in Game1.cs to process any input into movement on the screen. My method is called ProcessMovement and you will need to pass it parameters for the KeyboardState and the GamePadState:
public void ProcessMovement(KeyboardState keyState, GamePadState gamePadState)
{
}
We need to create a couple of variables to store the movement required, one is for the X scroll value and the other is for the Y scroll value:
public void ProcessMovement(KeyboardState keyState, GamePadState gamePadState)
{
//detect key press and xy scroll values
int scrollx = 0, scrolly = 0, moveSpeed = 2;
}
Now for processing the keyboard input:
public void ProcessMovement(KeyboardState keyState, GamePadState gamePadState)
{
//detect key press and xy scroll values
int scrollx = 0, scrolly = 0, moveSpeed = 2;
if (keyState.IsKeyDown(Keys.Left))
scrollx = -1;
if (keyState.IsKeyDown(Keys.Right))
scrollx = 1;
if (keyState.IsKeyDown(Keys.Up))
scrolly = 1;
if (keyState.IsKeyDown(Keys.Down))
scrolly = -1;
}
Now for the gamepad input, casting or conversion is required because gamePadState.ThumbSticks.Left.X gives a float and not an integer:
public void ProcessMovement(KeyboardState keyState, GamePadState gamePadState)
{
//detect key press and xy scroll values
int scrollx = 0, scrolly = 0, moveSpeed = 2;
if (keyState.IsKeyDown(Keys.Left))
scrollx = -1;
if (keyState.IsKeyDown(Keys.Right))
scrollx = 1;
if (keyState.IsKeyDown(Keys.Up))
scrolly = 1;
if (keyState.IsKeyDown(Keys.Down))
scrolly = -1;
scrollx += (int)gamePadState.ThumbSticks.Left.X;
scrolly += (int)gamePadState.ThumbSticks.Left.Y;
}
Now to use the scrollx & scrolly values to move the player in the map:
public void ProcessMovement(KeyboardState keyState, GamePadState gamePadState)
{
//detect key press and xy scroll values
int scrollx = 0, scrolly = 0, moveSpeed = 2;
if (keyState.IsKeyDown(Keys.Left))
scrollx = -1;
if (keyState.IsKeyDown(Keys.Right))
scrollx = 1;
if (keyState.IsKeyDown(Keys.Up))
scrolly = 1;
if (keyState.IsKeyDown(Keys.Down))
scrolly = -1;
scrollx += (int)gamePadState.ThumbSticks.Left.X;
scrolly += (int)gamePadState.ThumbSticks.Left.Y;
map.ObjectGroups["objects"].Objects["Player"].X += (scrollx * moveSpeed);
map.ObjectGroups["objects"].Objects["Player"].Y -= (scrolly * moveSpeed);
}
Now we need to add a few lines to call our method in the Update method. The gamePadState & keyboardState variables will have the player input, we pass this into the ProcessMovement method:
GamePadState gamePadState = GamePad.GetState(PlayerIndex.One);
KeyboardState keyState = Keyboard.GetState();
processMovement(keyState, gamePadState);
At this point the player should move around the map, but we haven't done anything to confine the player to the room.
Player Bounds
Create a new method in your Game1.cs called CheckBounds, we will need this method to return a boolean to identify if the player is within the bounds or not:
public bool CheckBounds()
{
bool check = false;
return check;
}
Now we need to identify the 4 corners of our player, we need to know the exact coordinates for each. The left top corner is easy because the X & Y of the player will give you this corner. The right top can be calculated by adding the Width onto the X of the player, the Y should be the same. The left bottom can be calculated by adding the height to the Y coordinate of the player. The right bottom can be calculated by adding the width to the X and the height to the Y:
public bool CheckBounds()
{
bool check = false;
//get exact coordinates for each corner
Vector2 lt = new Vector2(map.ObjectGroups["objects"].Objects["Player"].X, map.ObjectGroups["objects"].Objects["Player"].Y);
Vector2 rt = lt + new Vector2(map.ObjectGroups["objects"].Objects["Player"].Width, 0);
Vector2 lb = lt + new Vector2(0, map.ObjectGroups["objects"].Objects["Player"].Height);
Vector2 rb = lt + new Vector2(map.ObjectGroups["objects"].Objects["Player"].Width,
map.ObjectGroups["objects"].Objects["Player"].Height);
return check;
}
Now we know the location of each corner we can check to see which tile the corner is over. This can be calculated by dividing the X & Y coordinates by the tilepixel variable. We can add these new vectors into a list:
public bool CheckBounds()
{
bool check = false;
//get exact coordinates for each corner
Vector2 lt = new Vector2(map.ObjectGroups["objects"].Objects["Player"].X, map.ObjectGroups["objects"].Objects["Player"].Y);
Vector2 rt = lt + new Vector2(map.ObjectGroups["objects"].Objects["Player"].Width, 0);
Vector2 lb = lt + new Vector2(0, map.ObjectGroups["objects"].Objects["Player"].Height);
Vector2 rb = lt + new Vector2(map.ObjectGroups["objects"].Objects["Player"].Width,
map.ObjectGroups["objects"].Objects["Player"].Height);
//get the tile row and column for each corner
List<Vector2> corners = new List<Vector2>();
corners.Add(new Vector2((lt.X / tilepixel), (lt.Y / tilepixel)));
corners.Add(new Vector2((lb.X / tilepixel), (lb.Y / tilepixel)));
corners.Add(new Vector2((rt.X / tilepixel), (rt.Y / tilepixel)));
corners.Add(new Vector2((rb.X / tilepixel), (rb.Y / tilepixel)));
return check;
}
Now we know the tile position for each corner we can check if that tile in our collision layer tile is empty (0) or not. If the tile has a different value then we have collided with the bounds of the room:
public bool CheckBounds()
{
bool check = false;
//get exact coordinates for each corner
Vector2 lt = new Vector2(map.ObjectGroups["objects"].Objects["Player"].X, map.ObjectGroups["objects"].Objects["Player"].Y);
Vector2 rt = lt + new Vector2(map.ObjectGroups["objects"].Objects["Player"].Width, 0);
Vector2 lb = lt + new Vector2(0, map.ObjectGroups["objects"].Objects["Player"].Height);
Vector2 rb = lt + new Vector2(map.ObjectGroups["objects"].Objects["Player"].Width,
map.ObjectGroups["objects"].Objects["Player"].Height);
//get the tile row and column for each corner
List<Vector2> corners = new List<Vector2>();
corners.Add(new Vector2((lt.X / tilepixel), (lt.Y / tilepixel)));
corners.Add(new Vector2((lb.X / tilepixel), (lb.Y / tilepixel)));
corners.Add(new Vector2((rt.X / tilepixel), (rt.Y / tilepixel)));
corners.Add(new Vector2((rb.X / tilepixel), (rb.Y / tilepixel)));
//check if any corners are on a blocked tile
foreach (Vector2 corner in corners)
{
int tile = collision.GetTile((int)corner.X, (int)corner.Y);
if (tile != 0)
{
check = true;
}
}
return check;
}
Using CheckBounds
Now we need to edit the Update method of Game1.cs, firstly we want to record the current X & Y value of the player before we apply any movement. We can then use ProcessMovent to move the player, and then CheckBounds. If CheckBounds returns true we can use the stored X & Y values to move the player back to the position before the movement:
//store current position to move back too if collision
int tempx = map.ObjectGroups["objects"].Objects["Player"].X;
int tempy = map.ObjectGroups["objects"].Objects["Player"].Y;
processMovement(keyState, gamePadState);
//now we have moved checkbounds
if (CheckBounds())
{
map.ObjectGroups["objects"].Objects["Player"].X = tempx;
map.ObjectGroups["objects"].Objects["Player"].Y = tempy;
}
You should now have a player confined to the room you created on your map.
Collectables
Load your map again in Tiled, this time create a new object layer called Collectables. Then within this layer i have created 3 new objects called Coin_1 , Coin_2 , and Coin_3. I have also set the type to Coin and the height & width of each object to 16 pixels:
Now, click on the Collectables layer and in the properties panel add a custom property (+ symbol in bottom left corner). Make an integer called Coin_Count: