Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon


1. June 2015

 

Today we are going to look at creating 2D maps composed of tiles.  You can think of tiles as re-usable lego block like sprites that are assembled to create a more detailed map.  Tiles are stored in a data structure called a tileset, where collision details can be added.  These tiles and tile sets are then used to “paint” 2D scenes in something called a tilemap.  A tile map itself can contain multiple layers of tiles stacked on top of each other.  Don’t worry, it will make sense once we jump in.

 

WARNING!


When I wrote this tutorial, the majority of functionality I cover is currently under very active development. In order to follow along with this tutorial you need to have version 4.8 installed. Currently 4.8 is in preview release only, hopefully it will be released soon and I can remove this message. For now however, if you want to work with 2D tilemaps with collision data, you need to install the development release. For details on how to do this please read this post.

 

So, at this point I assume you either have the developer preview download, enough time has elapsed that this functionality is in the main release or you are simply reading on for future reference.  All the disclaimers out of the way, let’s jump in!

 

There is an HD video version of this tutorial available here: [Coming Soon].

 

Creating a Tileset

 

First start off by loading a sprite sheet texture in Unreal Engine, details of loading a sprite are available here.

For  this particular example, we need some tiles to work with.   Instead of creating my own spritesheets, I am going to use some of the free graphics that Kenney.nl makes available, specifically the Platform Pack.  Obviously you can use whatever image you wish, just be sure that the tiles are size the same and ideally that your image is a power of two in size. 

Import the spritesheet you are going to use for your tiles, in my case I selected Spritesheets/spritesheet_ground.png.  Make any changes you wish to the texture, such as disabling mipmaps and turning filtering to nearest.

Now right click your newly created texture and select Sprite Actions->Create Tileset:

image

 

This will then create a TileSet object, double click it to open the editor.

image

 

The TileSet editor should appear:

image

 

Across the left hand side are all of the tiles that are in your imported sprite.  Selecting one will make it visible in the top right window.  The bottom right window has properties for the entire texture set.  The most important to set right away is the Tile Size:

image

 

Here you enter the pixel dimensions of each individual tile within your image.  In the spritesheet from Kenney.nl, each tile is 128x128 in size.  The remaining settings are for tilesets that have gaps between tiles and aren't applicable in this case.  Both the left and top right window can be zoomed and panned using the regular commands.

 

Now let’s look at setting up collision shapes for a few tiles.  First select a tile from the left side, like so:

image

 

A white rectangle will bound around the selected tile.  It will now appear in the top right window:

image

 

We can now define bounding shapes using the toolbar:

image

 

In this case, a simple box is the easiest ( and least processing intensive.  So click Add Box:

image

 

This will now make it so the entire surface causes a collision.   For non-box shaped tiles, you are often going to want to use the Add Polygon option instead, then define the collision boundary accordingly, like so:

tile

 

Simply click for each vertices you wish to create.  Once done hit enter to finish your shape.  You can shift click to add new points to an existing shape.

Repeat this action for each tile that has collision data.  If a sprite is able to pass completely through the sprite without collision you don’t need to provide a collision shape at all.  Repeat this step for each tile in your set that can be collided with.

You can easily check which tiles you’ve defined a collision shape for by clicking Colliding Tiles:

image

 

When done click Save and we have just created our first TileSet.

 

Creating a TileMap

 

Now it’s time to create a Tilemap.   To create a tilemap select Add New –>Paper2D->Tile Map

image

 

This will create a new tile map object.  Double click it to bring up the tilemap editor.

image

 

Here is the tilemap editor in action:

image

 

On the left hand side is a selection of tiles you can paint with.  In the middle is the canvas you paint on, while on the right are your layer controls and the layer properties.  There are a couple critical things you need to configure right away.

 

First select your tileset.  On the left hand side, drop down the Active Tile Set dialog ( hit the grid icon ) and select the tile set we just created.

image

 

Now in the layer properties, we set the size of our tiles and the overall width and height of our layer ( in tiles ):

image

Start by selecting a base tile to fill the entire map with, select file mode and then click somewhere inside the map grid, like so:

 

Select base tile:

image

 

Choose Fill:

image

 

And click:

image

 

Now select an individual tile to paint with, click Paint, then draw it on the map, like so:

tile2

 

Quite often you are going to want tiles to appear “over” other tiles.  This can be accomplished using layers.  To add a layer simply click the Add New Layer button:

image

 

The order layers are drawn is the same as they are displayed:

image

You can use the up and down icons to change the layer order.  The layer selected ( the one highlighted ) is the layer that all drawing will occur on.

 

Adding your Tilemap to the Scene

 

Now that you’ve created your map, you can use it like you would any Sprite object.  Simply drag it into your scene:

tile3

 

The positioning of the tilemap is important, the Y value is going to determine what is drawn over or under when drawing the scene, just like with sprites.  In this case however, sometimes you want to position your sprite in front of the background, but behind a foreground layer, like so:

tile4

 

This is done using a property called Separation Per Layer in the Tilemap details.

 image

This is the Y coordinate ( confusingly called Z order in the tooltip ) of the layer within the game world.  For example if you position your tilemap at Y= –10 and set Separation Per Layer to 50, the first layer will be at Y=40, the second at Y=90, etc.  Therefore a sprite at 0 will draw in front of the bottom layer, but behind the top layer.

If you want to see a more detailed example showing collisions in action, be sure to watch the video version of this tutorial.

 

The Video

 

Coming soon

Programming , ,

blog comments powered by Disqus

Month List

Popular Comments

Network programming with SFML and Node.js: Part 3
Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon


14. March 2012

 

 

In part 1 we look at a very basic example of sending information across the wire from SFML to NodeJS.  In part 2 we looked at JSON encoding high score data, that is then sent via UDP socket to a Node server.  Now we complete the process, by having Node return high score data back to your C++ SFML app.  By the end of this section, you will have all the code you need for a functioning ( if somewhat fragile ) high score client and server.

 

 

First we look at the C++ code.  Things are very similar to part 2, except code has been refactored a bit for re-use.  Lets look now.

 

 

#include "SFML/Network.hpp" #include "JSON.h" #include <iostream> void MakePacket(const wchar_t* action, const wchar_t * name, const float score, sf::Packet &packet) { JSONObject data; data[L"action"] = new JSONValue(action); data[L"name"] = new JSONValue(name); data[L"score"] = new JSONValue(score); JSONValue * val = new JSONValue(data); data.clear(); std::wstring dataString = val->Stringify(); delete val; std::string notSoWide; notSoWide.assign(dataString.begin(),dataString.end()); packet.Append(notSoWide.c_str(),notSoWide.length()); } int main(int argc, char* argv[]) { sf::IPAddress ip("127.0.0.1"); sf::SocketUDP socket; sf::Packet packet; unsigned short port = 1000; unsigned short respPort = 1001; if(argc == 1) { //No arguments means program should retrieve scores and print them MakePacket(L"GetScores",L"",0.0f,packet); socket.Bind(respPort); socket.Send(packet,ip,port); char buffer[512]; // The buffer to store raw response data in sf::IPAddress respIP; // The ip address where the response came from size_t respSize; // The amount of data actually written to buffer // Now receive a response. This is a blocking call, meaning your program // will hang until a response is received. socket.Receive(buffer,512,respSize,respIP,port); socket.Close(); std::string respString(buffer,respSize); // Now lets turn the string back into JSON JSONValue * jsonHighScores = JSON::Parse(respString.c_str()); if(!jsonHighScores->IsObject()) { std::cout << "Something went wrong, not good."; return -1; } JSONObject root = jsonHighScores->AsObject(); if(root.find(L"Scores") != root.end() && root[L"Scores"]->IsArray()) { JSONArray scores = root[L"Scores"]->AsArray(); std::cout << "Current high scores:" << std::endl; for(int i = 0; i < scores.size();i++) { JSONObject curObj = scores[i]->AsObject(); std::wcout << "Name:" << curObj[L"Name"]->AsString(); std::cout << " High Score:" << curObj[L"Score"]->AsNumber() << std::endl; } } delete jsonHighScores; } else if(argc == 3) { MakePacket(L"AddScore", std::wstring(argv[1],argv[1] + strlen(argv[1])).c_str(), atof(argv[2]), packet); if(socket.Send(packet,ip,port) != sf::Socket::Done) { std::cout << "An error ocurred sending packet" << std::endl; } socket.Close(); } else { std::cout << "Invalid usage, proper format is player name then score, for example:" << std::endl; std::cout << "Or run with no arguments to get a list of scores returned" << std::endl; std::cout << "Scoreboard \"Player Name\" 42" << std::endl; return -1; } return 0; }

Click here to download Scoreboard.cpp

 

 

We have reorganized slightly to use a set of if/else’s based on the number of parameters passed in.  The common code between the two handled conditions has been moved to the method MakePacket(), which contains nothing new from last part.  It’s the section where argc == 1 ( which means there were no parameters specified, as argc at 1 represents the executables name ) that we are interested in.  If the user runs the application from the command line with no parameters, we want to fetch the high scores from the server.

 

 

The request process is the same, although we are passing the action GetScores instead.  One key difference is we Bind our port.  Think of this action as say “Yo!  This port is ours!”.  Only one application per computer can have access to a given port.  This is why we run our server on 1000, but then bind our response port on 1001, since client and server on running on the same machine.  Unlike when we add a score, for GetScores we want to listen for a response, which is what we do with socket.Receive().  Keep in mind, this action blocks, so your program wont be able to continue until this is done.  There are alternatives ( like Selector ) if this behavior is undesirable.

 

 

Now assuming Receive() worked correctly ( which in the Real World™ you shouldn’t!), buffer will be full of our JSON encoded string, while respSize will represent the amount of buffer that was actually used.  Using these two pieces of information, lets create a string from only the meaningful bits ( the rest of the buffer will be full of gibberish ).  We now turn our string back into JSON ( in production code, I would extend the JSON library to work with standard strings ), and check to see if it is a valid JSON object, error out if it’s not.  Now it’s a matter of parsing out the JSON into meaningful form.  Remember that a JSONObject is actually just a STL map of key/value pairs, so we find our array of type JSONArray named Scores.  Each item in that array is in turn another map, so we loop through them all find the value for “Name” and “Score”, turn them back into their native type and display them out to the console.  And that’s about it.

 

 

Now lets take a look at the server side of things.  Here we made much less invasive changes, so lets just look at what has changed.  To see the fully modified server.js click here.  I no doubt forgot a small change here or there, I always do!

 

 

First we add another condition to the switch statement as follows:

 

case "GetScores": console.log("Get Scores called"); // Turn highscores back into a JSON encoded string var highScoreAsString = JSON.stringify(highScores); // Create a buffer to hold that string var responseBuffer = new Buffer(highScoreAsString.length); // Write the string to the buffer responseBuffer.write(highScoreAsString); // Send it back to the client, using the addressing information // passed in via rinfo server.send(responseBuffer,0,responseBuffer.length, rinfo.port,rinfo.address, function(err, sent){ if(err) console.log("Error sending response"); else console.log("Responded to client at " + rinfo.address + ":" + rinfo.port ); }); break;

 

 

The code is pretty much documented, the nutshell version is, we turn our high score information into a JSON encoded string, make a buffer large enough for the string, copy the string to the buffer, then call send to the address and port the request came from, as defined in rinfo.

 

 

Then, mostly because we can, we use Node to create an extremely simple high score web server, so if you visit this site with your browser you can get a current list of high scores.  Look how laughably easy that task is!

 

 

// Now, lets show off a bit, with the worlds simplest web server. // Dynamically creates webpage showing current high scores. var webServer = require('http'); webServer.createServer(function(req,res){ res.writeHead(200, {'Content-Type': 'text/html'}); res.write("<html><body><h1>High Scores</h1><ul>"); for(i=0;i < highScores.Scores.length;++i) { res.write(highScores.Scores[i].Name + "&nbsp;&nbsp;" + highScores.Scores[i].Score + "<br />"); } res.write("</ul></body></html>"); res.end(); }).listen(80);

 

 

 

And… that’s it!  So now, lets take a look at things in action.  Open a command prompt and launch your server.js in node.  Then in another command prompt run scoreboard.exe, like this:

 

 

Here is using Scoreboard from the command line:

image

 

 

And here is the server handling these requests:

 

image

 

 

Finally, fire up a web browser and hit 127.0.0.1 ( assuming you don’t have another webserver running on your machine ):

 

image

 

 

It ain’t pretty, but it is a fully functioning high score server.  All you need to do is add a layer of security, harden things a bit with an iota of fault tolerance, pretty things up a bit and you are set.

 

 

As always, you can download the complete project zip here.

Programming ,

blog comments powered by Disqus

Month List

Popular Comments