Difference between revisions of "Simple Racer"
(→The Car) |
|||
(8 intermediate revisions by the same user not shown) | |||
Line 27: | Line 27: | ||
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. The tiles i will use are 128 x 128 pixels, this will create quite a large map but we are only going to show the area around the car and not the whole map at once. You should be able to leave everything else the same. | 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. The tiles i will use are 128 x 128 pixels, this will create quite a large map but we are only going to show the area around the car and not the whole map at once. You should be able to leave everything else the same. | ||
− | |||
− | |||
− | |||
− | |||
==The map itself== | ==The map itself== | ||
Line 128: | Line 124: | ||
<syntaxhighlight lang=c#> | <syntaxhighlight lang=c#> | ||
Texture2D texture; | Texture2D texture; | ||
− | |||
Vector2 position; | Vector2 position; | ||
Vector2 origin; | Vector2 origin; | ||
Line 181: | Line 176: | ||
==Create a car in Game1== | ==Create a car in Game1== | ||
− | Add the following lines to declare your car, this should be | + | Add the following lines to declare your car, this should be with your other Game1 variables at the start of your code: |
<syntaxhighlight lang=c#> | <syntaxhighlight lang=c#> | ||
Line 188: | Line 183: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Now in LoadContent add the following to create the car itself: | + | Now in LoadContent add the following to create the car itself. The carPos should center the car on the screen because we find the center and take away half the width & height of the car: |
<syntaxhighlight lang=c#> | <syntaxhighlight lang=c#> | ||
Line 219: | Line 214: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Now in your GetInputs method add the following to handle the Up and Down arrow. This can go after your code to handle the steering input. The AND section of each if statement is to set a maximum speed. The if statement is to bring the car to a halt if nothing is pressed: | + | Now in your GetInputs method add the following to handle the Up and Down arrow. This can go after your code to handle the steering input. The AND section of each if statement is to set a maximum speed. The final if statement is to bring the car to a halt if nothing is pressed: |
<syntaxhighlight lang=c#> | <syntaxhighlight lang=c#> | ||
Line 351: | Line 346: | ||
coordinates.Add(new Vector2(-(texture.Width / 2), -(texture.Height / 2))); | coordinates.Add(new Vector2(-(texture.Width / 2), -(texture.Height / 2))); | ||
coordinates.Add(new Vector2(texture.Width / 2, -(texture.Height / 2))); | coordinates.Add(new Vector2(texture.Width / 2, -(texture.Height / 2))); | ||
− | coordinates.Add(new Vector2(-(texture.Width / 2), (texture.Height / 2))); | + | coordinates.Add(new Vector2(-(texture.Width / 2), texture.Height / 2)); |
+ | </syntaxhighlight> | ||
+ | |||
+ | You could add additional points to improve your collision detection later. You could also adjust the points to more closely match the corners of your car, for example: | ||
+ | |||
+ | <syntaxhighlight lang=c#> | ||
+ | coordinates.Add(new Vector2((texture.Width / 2)-5, (texture.Height / 2)-5)); | ||
+ | coordinates.Add(new Vector2(-(texture.Width / 2)+5, -(texture.Height / 2)+5)); | ||
+ | coordinates.Add(new Vector2((texture.Width / 2)-5, -(texture.Height / 2)+5)); | ||
+ | coordinates.Add(new Vector2(-(texture.Width / 2)+5, (texture.Height / 2)-5)); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | Now for each of the points in the coordinates list we need to do more maths, in order to rotate points around the origin we need to use sine & cosine again: | |
<syntaxhighlight lang=c#> | <syntaxhighlight lang=c#> | ||
Line 440: | Line 444: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Now inside the CheckBounds method, and between the list declaration and the return call, we need to check each corner. We need to add the position of the car to the corner to get its position in the map. We then need to convert this into which tile the corner is currently over. so add: | + | Now inside the CheckBounds method, and between the list declaration and the return call, we need to check each corner. We need to add the position of the car to the corner to get its position in the map. We then need to convert this into which tile the corner is currently over. we divide the X & Y of the corner by the width of each tile (128) so add: |
<syntaxhighlight lang=c#> | <syntaxhighlight lang=c#> |
Latest revision as of 15:10, 21 January 2024
This tutorial will show you how to create a top down racing game, you will create a track, and your car will be able to complete a lap, time the lap, and potentially to complete a set number of laps. It will also cover simple AI to make a ghost car drive around your circuit.
You could go on to learn about adding traction or skidding, adding collectables or weapons
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. The tiles i will use are 128 x 128 pixels, this will create quite a large map but we are only going to show the area around the car and not the whole map at once. You should be able to leave everything else the same.
The map itself
I have created a map with 3 different tile layers, one for the collision which includes all of the tyres, a track layer which only contains the track peices, and a layer for the mud or grass for the background.
I have also created an object layer with the players starting location, the 2 check points and the start line all stored as objects. I have also created a custom property for the number of checkpoints (this may change depending on the track).
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
Or get it from GitHub: GitHub TRCCompSci tiled-xna
Remember to set the name space to Squared.Tiled.
You will need to add references in the using section for the following:
using System.IO;
using Squared.Tiled;
While you add those you may as also add the following for the use of List later:
using System.Collections.Generic;
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;
Game1 constructor
I have set the width and height in the constructor:
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
graphics.PreferredBackBufferWidth = 1280;
graphics.PreferredBackBufferHeight = 720;
}
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, "SimpleRacer.tmx"), Content);
collision = map.Layers["Collision"];
tilepixel = map.TileWidth;
viewportPosition = new Vector2(map.ObjectGroups["Objects"].Objects["Player"].X - 640, map.ObjectGroups["Objects"].Objects["Player"].Y-360);
The viewport position takes the X & Y of the player and subtracts half the width from the X and half the height from the Y.
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 and display your map centered onto the player location.
The Car
Create a new class in your project and call it Car.cs.
We will need to add the following to the using section of your new class:
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.IO;
using System.Collections.Generic;
Now in the Car class declare the following variables:
Texture2D texture;
Vector2 position;
Vector2 origin;
float steer =0;
Now we need to create an init method to set up the car. This accepts the texture, position of the car and also calculates the origin point of the car (we will use this to rotate the car). The origin will be the center of the player hence dividing the width and height by 2:
public void Init(Texture2D t, Vector2 pos)
{
position = pos;
texture = t;
origin = new Vector2(t.Width / 2, t.Height / 2);
}
Now create a new method called GetInputs, this is used to control the car. It accepts the KeyboardState, which is checked for the key pressed. If the left arrow is pressed we are going to minus 0.05 from the current steer value. If the right arrow is pressed we are going to ad 0.05 onto the steer value:
public void GetInputs(KeyboardState input)
{
if (input.IsKeyDown(Keys.Left))
{
steer -= (.05f);
}
if (input.IsKeyDown(Keys.Right))
{
steer += (.05f);
}
}
Now create an Update method, it needs to accept the gameTime passed, the only code you need inside is to run the CheckInputs method above:
public void Update(GameTime gameTime)
{
GetInputs(Keyboard.GetState());
}
Finally we need to add a Draw method. This is more complex than before because we need to rotate the texture. The FlipHorizontally effect was just to ensure my car faced the correct way, you could instead use SpriteEffects.None:
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(texture, center,null,null,center,steer,null,null,SpriteEffects.FlipHorizontally,0);
}
Create a car in Game1
Add the following lines to declare your car, this should be with your other Game1 variables at the start of your code:
Texture2D cartexture;
Car car;
Now in LoadContent add the following to create the car itself. The carPos should center the car on the screen because we find the center and take away half the width & height of the car:
cartexture = Content.Load<Texture2D>("car");
Vector2 carPos = new Vector2((graphics.PreferredBackBufferWidth/2)-(cartexture.Width/2),
(graphics.PreferredBackBufferHeight / 2)-(cartexture.Height/2));
car = new Car();
car.Init(cartexture, carPos);
in the Update method of Game1.cs add the following to update the car:
car.Update(gameTime);
Finally we need to add the following line to draw the car, make sure it goes after the line to draw the map but before the spriteBatch.End():
car.Draw(spriteBatch);
If you test it now, your car should be in the center and it rotates with the arrow keys.
Adding forward & backward inputs
Within your Car class add a new variable for speed:
float speed = 0;
Now in your GetInputs method add the following to handle the Up and Down arrow. This can go after your code to handle the steering input. The AND section of each if statement is to set a maximum speed. The final if statement is to bring the car to a halt if nothing is pressed:
if (input.IsKeyDown(Keys.Up) && speed < 15)
{
speed += 2;
}
if (input.IsKeyDown(Keys.Down) && speed > -15)
{
speed -= 1;
}
if (speed > 0)
speed -= 1;
else if (speed < 0)
speed += .5f;
else
speed = 0;
Now in your update method add the following to update the position of the car. Remember we always draw the car in the center of the screen, this position should be within the map. This is a bit of complex maths, it is to essentially to find the length of a triangle sides based on the angle of steer and the length of the hypotenuse (speed):
position = position + new Vector2(speed * (float)Math.Cos(steer), speed * (float)Math.Sin(steer));
We are going to need get the position of the Car in the Game1.cs so that we can move the map to the right position. So in your Car class create a new property. This should be inside the class but not within any method:
public Vector2 Position
{
get { return position; }
set { position = value; }
}
Now to move the map
In the LoadContent method in Game1.cs we need to change this line:
viewportPosition = new Vector2(map.ObjectGroups["Objects"].Objects["Player"].X - 640, map.ObjectGroups["Objects"].Objects["Player"].Y-360);
We want to use this to set the position of the car in the map instead, so change it to this. Make sure this line is after Car.Init():
car.Position = new Vector2(map.ObjectGroups["Objects"].Objects["Player"].X - 640, map.ObjectGroups["Objects"].Objects["Player"].Y-360);
Now in the Draw method for Game1.cs we need to use Car.Position to move the map so change this line:
map.Draw(spriteBatch, new Rectangle(0, 0, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight), viewportPosition);
to this:
map.Draw(spriteBatch, new Rectangle(0, 0, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight), car.Position);
Movement tweeks
Try your current movement, in reverse the steer directions need to be reversed, So the simple if statement below is used to set the value of swap, this is then used to multiply the steer value:
int swap = 1;
if (speed < 0)
swap = -1;
Also we only want to rotate the car if the car is currently moving. change the following if statements:
if (input.IsKeyDown(Keys.Left) && speed != 0)
{
steer -= (.05f * swap);
}
if (input.IsKeyDown(Keys.Right) && speed != 0)
{
steer += (.05f * swap);
}
Detecting Collisions
Set coordinates for each corner
This needs again some complex maths to rotate each corner coordinate around an origin. I found this maths from the internet after searching "rotate point around origin". This needs to go into the Car class.
Declare a list for corners
Add this declaration to the Car class:
List<Vector2> corners = new List<Vector2>();
We also need to create a new vector to represent the center of the car:
Vector2 center;
Also create a new property to return the corners list. This should go within the Car class but not within a method:
public List<Vector2> Corners
{
get { return corners; }
}
Change the Init method
We need to setup the center vector in the Init method of the car class:
center = position + new Vector2(t.Width / 2, t.Height / 2);
position should be the left top corner of your car so adding half the height and width should give you the center.
Change the update method
We now need to change the Update method of your Car class. We will need to run this repeatedly so we will clear the corners list, these will be different everytime we rotate the car. We also need to set the X & Y for the center, we then create a list of the 4 corners written as a vector from the center point.
corners.Clear();
float cx = center.X;
float cy = center.Y;
List<Vector2> coordinates = new List<Vector2>();
coordinates.Add(new Vector2(texture.Width / 2, texture.Height / 2));
coordinates.Add(new Vector2(-(texture.Width / 2), -(texture.Height / 2)));
coordinates.Add(new Vector2(texture.Width / 2, -(texture.Height / 2)));
coordinates.Add(new Vector2(-(texture.Width / 2), texture.Height / 2));
You could add additional points to improve your collision detection later. You could also adjust the points to more closely match the corners of your car, for example:
coordinates.Add(new Vector2((texture.Width / 2)-5, (texture.Height / 2)-5));
coordinates.Add(new Vector2(-(texture.Width / 2)+5, -(texture.Height / 2)+5));
coordinates.Add(new Vector2((texture.Width / 2)-5, -(texture.Height / 2)+5));
coordinates.Add(new Vector2(-(texture.Width / 2)+5, (texture.Height / 2)-5));
Now for each of the points in the coordinates list we need to do more maths, in order to rotate points around the origin we need to use sine & cosine again:
foreach (Vector2 point in coordinates)
{
//x, y - coordinates of a corner point of the square
float x = cx - point.X;
float y = cy + point.Y;
// translate point to origin
float tempX = x - cx;
float tempY = y - cy;
// now apply rotation
float rotatedX = (float)(tempX * Math.Cos(steer) - tempY * Math.Sin(steer));
float rotatedY = (float)(tempX * Math.Sin(steer) + tempY * Math.Cos(steer));
// translate back
x = rotatedX + cx;
y = rotatedY + cy;
corners.Add(new Vector2((int)x, (int)y));
}
This will now add the four corner points into a list called corners. For debugging purposes it would be a good idea to draw something at these points so we can see them. So declare a new Texture2D in the car class:
Texture2D marker;
We need to change the Init method of your car class to accept a texture for the marker. So we need to change the Init method to this:
public void Init(Texture2D t, Texture2D m, Vector2 pos)
{
position = pos;
texture = t;
marker = m;
center = position + new Vector2(t.Width / 2, t.Height / 2);
origin = new Vector2(t.Width / 2, t.Height / 2);
}
Now in the Draw method of your Car class add these to draw the marker at the center and then at each point, make sure you add this after the current line to draw the car:
spriteBatch.Draw(marker, center, Color.Purple);
foreach (Vector2 corner in corners)
spriteBatch.Draw(marker, corner, Color.Red);
Now in the Game1.cs we need to change the LoadContent method to load in a texture for marker. My texture is a 5 pixel by 5 pixel image i created on paint, I filled it with a single colour. This code reads the texture into the Init method call:
car.Init(cartexture, Content.Load<Texture2D>("marker"), carPos);
Now when you run your game you should have a marker in the center, and a marker on each corner.
Check collision layer
Load in the collision layer
In the declaration section of Game1.cs create a variable to store the layer:
Layer collision;
Now in the LoadContent of Game1.cs, and make sure you put this line after the one to read in your map:
collision = map.Layers["Collision"];
CheckBounds method
We now need to create a new method called CheckBounds within the Game1.cs. This method will return a boolean. We will first declare a boolean and set it to be false. This boolean will be returned by the method at the end, we also need to get the corners for the car. Remember we created a property to get this list from the Car class:
public bool CheckBounds()
{
bool check = false;
List<Vector2> corners = car.Corners;
return check;
}
Now inside the CheckBounds method, and between the list declaration and the return call, we need to check each corner. We need to add the position of the car to the corner to get its position in the map. We then need to convert this into which tile the corner is currently over. we divide the X & Y of the corner by the width of each tile (128) so add:
foreach(Vector2 cr in corners)
{
Vector2 corner = cr + car.Position;
int i = (int)corner.X / 128;
int j = (int)corner.Y / 128;
}
Now we have the tile coordinates we need to check it The code below needs to go after the two integer declarations but before the closing curly bracket after them. The first if statement checks if the tile is on the map, if not it will set check to true (ie it is out of bounds). The inner if statement gets the tile value for the tile, and if it is 0 then no tile is on that square. So if the tile is not equal to zero then we need to collide with it. We set check to true to show we have collided:
if (i > 0 && i < 30 && j > 0 && j < 15)
{
if (collision.GetTile(i, j) != 0)
{
check = true;
}
}
else check = true;
Your final CheckBounds method should look like this:
public bool CheckBounds()
{
bool check = false;
List<Vector2> corners = car.Corners;
foreach(Vector2 cr in corners)
{
Vector2 corner = cr + car.Position;
Rectangle cornerrec = new Rectangle((int)corner.X, (int)corner.Y, 0, 0);
int i = (int)corner.X / 128;
int j = (int)corner.Y / 128;
if (i > 0 && i < 30 && j > 0 && j < 15)
{
if (collision.GetTile(i, j) != 0)
{
check = true;
}
}
else check = true;
}
return check;
}
Finally we need to change the Update method of Game1.cs. We need to store the car position before we update the car. Then once we have updated the car, we can create an if statement which runs CheckBounds. If it returns true it will move the car back to before any move:
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
Exit();
Vector2 carPos = car.Position;
car.Update(gameTime);
// TODO: Add your update logic here
if (CheckBounds())
{
car.Position = carPos;
}
base.Update(gameTime);
}
You should now collide with the tiles in the collision layer, test it and see. The markers should allow you to see when your car hits the tile.
CheckPoints
Read in checkpoints
In the Game1.cs add the following declarations to store the check points:
Rectangle[] checkpoints;
int checkpoint = -1;
Now in LoadContent of Game1.cs we need to get the checkpoints from the map. This gets the number of check points from the map, and uses this value plus 1 to create the array of rectangles. The first checkpoint is really the start line so read the position, width and height. These are used to create the element 0 rectangle:
int checkCount = Convert.ToInt32(map.ObjectGroups["Objects"].Properties["CheckPoints"]);
checkpoints = new Rectangle[checkCount + 1];
int x = map.ObjectGroups["Objects"].Objects["StartLine"].X;
int y = map.ObjectGroups["Objects"].Objects["StartLine"].Y;
int w = map.ObjectGroups["Objects"].Objects["StartLine"].Width;
int h = map.ObjectGroups["Objects"].Objects["StartLine"].Height;
checkpoints[0]=new Rectangle(x, y, w, h);
Now we can use a loop to read in the remaining checkpoints:
for (int i = 1; i <= checkCount; i++)
{
x = map.ObjectGroups["Objects"].Objects["CheckPoint" + i].X;
y = map.ObjectGroups["Objects"].Objects["CheckPoint" + i].Y;
w = map.ObjectGroups["Objects"].Objects["CheckPoint" + i].Width;
h = map.ObjectGroups["Objects"].Objects["CheckPoint" + i].Height;
checkpoints[i] = new Rectangle(x, y, w, h);
}
Checking checkpoints
Now create a new method in Game1.cs called CheckPoints. We need to declare a new list of Vector2 to store what is returned by car.Corners. Now we need a for loop to loop through each checkpoint. For each checkpoint we will check each corner of the car:
public void CheckPoints()
{
List<Vector2> corners = car.Corners;
for (int i = 0; i < checkpoints.Length; i++)
{
foreach (Vector2 corner in corners)
{
}
}
}
Now within the foreach loop above add the following code. This code creates a rectangle for the corner and then checks if this intersects the checkpoint rectangle. If it does the integer checkpoint should equal the value of the previous checkpoint, then we set checkpoint to the new value:
Vector2 cr = corner + car.Position;
Rectangle vectorrec = new Rectangle((int)cr.X, (int)cr.Y, 1, 1);
if (vectorrec.Intersects(checkpoints[i]))
{
if (checkpoint == (i - 1))
{
if (i == (checkpoints.Length-1))
checkpoint = -1;
else
checkpoint = i;
Console.WriteLine("CheckPoint "+(i));
}
}
if it is the last checkpoint before the start line we set checkpoint to -1. Finally we need to add the code to the Update method to run CheckPoints. So add the following to the Update method:
CheckPoints();
This line can go after car.Update but before the CheckBounds if statement. Test your game and it should write to the console everytime it goes through a checkpoint.
Timing
Declaring variables
To use a stopwatch in c# we need to add the following to the using section of Game1.cs:
using System.Diagnostics;
Now we can declare a stopwatch for the lap and overall race time. I have also declared an integer to count the number of laps:
Stopwatch lapTime = new Stopwatch();
Stopwatch raceTime = new Stopwatch();
int lapcount = 0;
Checking for start line
we are going to change the CheckPoints method, and this line:
Console.WriteLine("CheckPoint "+(i));
Instead we are going to check if the checkpoint was the start. If it was the start we should check if the stopwatch is running, if not we should start the stopwatch. If the stopwatch is running then this is a completed lap so output the laptime, reset the laptime and restart it:
if (i == 0)
{
if (raceTime.ElapsedTicks == 0)
{
Console.WriteLine(lapTime.Elapsed);
lapTime.Start();
raceTime.Start();
}
else
{
lapcount++;
Console.WriteLine("Lap " + lapcount);
Console.WriteLine("Lap Time " + lapTime.Elapsed);
lapTime.Reset();
lapTime.Start();
Console.WriteLine("Race Time " + raceTime.Elapsed);
}
}
else
Console.WriteLine("CheckPoint "+(i));