Today we completed our Character Controller, and then took a deep dive into Sprites. We examined how to import and properly configure these images, split Sprite Sheets into multiple sprites, how to convert sprite into Tiles, and finally how to make levels using Tilemaps.
Part 1: Finishing the Collider
At the end of yesterday’s class, we had set up most of our 2D Character Controller, making modifications from a script that we had found. One aspect that we did not get to complete was to simplify the means of generating the “ground” detection.
Rather than remeasuring the collider bounds and recalculating the sizes in each fixed update, we instead created a child object that we will use as our “Ground Check” object. We add public variables to our script to hold the Transform of that child object (in order to grab the position) and a float value to measure the radius.
Next, we added a Gizmo to draw a sphere representing that ground check object so that we can see our area of effect and create the proper amount of protrusion just below the curvature of our player collider.
private void OnDrawGizmos() { // Draw a yellow sphere at the ground check position Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(groundCheckObject.position, colliderRadius); }
Part 2: Sprites
“Sprite” is a term that has been used in the video game industry almost since it’s inception. In the beginning, it referred to an image of a specific size and type that could be drawn directly to the the screen without having to be processed by the CPU, and was used to improve the performance of a game. These days, the term refers to any bitmap (or image) that is used primarily as a 2D object in a game, rather than a texture or mesh object. 2D games still rely heavily on these for their characters and worlds.
Technically, our Sprite objects in Unity are still mesh objects. Unity generates a simple flat mesh shape, and applies the bitmap to it as a texture, but instead uses an optimized shader to draw this without considering many of the procedure we would include in 3D rendering. Even our text objects generate a simplified mesh and apply the font as a texture to it. For our purposes, however, we will refer to “mesh” objects as those belonging in the 3D realm, and our “sprite” object meshes simply as Sprites.
A “sprite sheet” refers to an image that contains a collection of images to be used as sprites. Sometimes these are collections of common objects or “tiles” that can be used to create a level, as we will do here, but more often you will encounter them as collections of sequential frames for animation. The reason that these sprite sheets are used is that it is far more costly to load a number of small images into memory and swap them in and out as objects in the scene than it is to have a single large image and instead adjust which coordinates of the image will be drawn in a particular frame.
For this demo, we are going to use three files as sprites:
In this case, we have:
- Our hero, a little Viking Boy (a high resolution drawing, and part of a larger sprite sheet comprised of frames of animation. I’ll be showing these next week)
- An enemy (a very small pixelated dragon, also from a sprite sheet comprised of frames of animation)
- Environment Tiles (a 128×128 sprite sheet composed of 16×16 pixelated blocks, each one intended as a “Tile” for our Tilemap)
Sprite Layering
We start by placing an instance of our Viking Boy into the Scene. You’ll notice when we do this, that we have a new Component, the Sprite Renderer. (This replaces the Mesh Renderer and Mesh components from our 3D games.) You will also notice that the default tool to move the Sprite is the Rect Tool, located to the right of the Scale button. (The hotkey for this is: Y) You can still use move/scale/rotate, but the Rect tool is easier for 2D workflows.
Our Sprite Renderer component gives us access to only a few controls that we will use. The “flip” property is useful for swapping a sprites direction without resorting to rotation or scaling tricks. The “color” property lets you set a tint for the sprite, as well as the alpha (transparency) value. This means that any sprite can be faded. There are no special shaders like what we had to use with our 3D blocks in the last assignment.
Under Additional Settings we find perhaps the most important setting, the Sorting Layer. Unity’s sprite rendering works a little different from what you have experienced up to this point, in that it often doesn’t matter how close or far a sprite is from the camera – the order that sprites are drawn in (and therefore the determination of which sprite will be in front of the other) comes down to a series of comparisons, the most important of which is the Sorting Layer.
Here, we have our Viking Boy on the “Default” layer, but if we access the dropdown and click “Add Sorting Layer…” we go to our Tags & Layers panel and can create new layers. The order of these layers determines which will draw in front of which. Each layer on the list will draw in front of the layer before it, so the objects set to the bottom layer (here we created “Foreground”) will draw in front of everything else.
Also in our Additional Settings, you can see the Order within Layer setting. This takes a numerical value, and when two objects in the same layer overlap, the object with the higher number will render in front of the lower numbered object. (If objects are equal after this, distance to camera is finally considered)
Importing and Editing Sprite Sheets
Sprites and sprite sheets come into our game the same way as the rest of our resources – we import them using Assets> Import New Assets…
Once imported, you will want to select it so that you can make edits to the Import Settings. Remember, these assets are external to your editor. Nothing you do here will change the contents of the sprite sheet, only how the Editor interprets it. Also remember that once you have made changes you will need to confirm the changes by clicking the Apply button at the bottom of the panel.
In your Import Settings, chance the Texture Type to “Sprite (2D and UI”. This tells Unity a bit about how you intend to use the file. If your sprite is a single image, then the default Sprite Mode setting of “Single” will work for you. However, if this is a sprite sheet, change this to “Multiple”, which lets unity know to expect more than one object will come out of this texture atlas. Your Pixels Per Unit setting serves as the scale for how big your sprites should be if you drop them into the scene (of course you can always scale them to meet your preference). By default this value is 100 pixels to a unit. This is great for our Viking Boy as this will make him about 2 units high, but our Environment object is only using 16×16 pixels per sprite. We change this value to “16” so that each block will be 1 unit by 1 unit.
Next, we want to correct the “fuzzy” look of our smaller objects. If we post this to our level, we see the edges are blurry. This is because Unity is “sampling” the image to visualize it at a higher resolution. This is great for things like photorealistic textures, but terrible for our pixel art. To correct this, we go to the Advanced Settings and set the Filter Mode to “Point (no filter)”. This setting controls how to handle scaling images. Using “point” tells the editor that we want the images sharp edges to be preserved.
If you are using pixel art, and are still getting images that don’t look quite right (i.e. the edges are sharp, but the sprites themselves are blocky or the colors somehow.. wrong) you might be having issues with the “compression” settings. Unity automatically puts compression on all images that it imports, but for pixel art this can often create a degraded look, and the gains are super minimal because the source images are so small to begin with. To correct this, look at the Default settings at the bottom of Import Settings and set your Compression to “None”. This will remove any adjustment and use your source material as-is.
Now it is time to split the sprites, which we will do with the Sprite Editor, which you access from the sprite’s Import Settings.
NOTE: If you want to use the Sprite Editor with the 3D template, you may need to install the 2D Sprite package, which you can do from the Package Manager. If you are starting from a 2D template, this package is already activated.
For our Environment sprite sheet, go to the Slice dropdown button at the top and select Grid by Cell Count. This will let you define the how the image should be split into pieces. For this object, we want to set the count to 8 columns and 8 rows. Keep the pivot at “Center” (you’ll want this for proper placement in the Tilemap.) Once you are ready, hit slice. You will see that small boxes have been drawn for these, but also that any empty square has been removed. If you update your sprite sheet with more sprites, you may need to run the Sprite Editor again to include the new content.
If you click one of these boxes, you will get a Rect controller, and call up information about the sprite itself. You can adjust values, or change the name of the individual sprite.
When you have completed your set-up & edits, click the “Apply” button in the Sprite Editor window, and then also the “Apply” button in the Import Settings. Now in the Asset window your sprite sheet will have a small arrow in a circle next to it. Clicking on this arrow will expand the list to show the individual sprites contained within this object.
Part 3: Tilemaps
Now that we have set up our sprites, it is time to build a level. While we could place individual sprites and colliders throughout the environment, Unity gives us an excellent tool for designing sprite content – the Tilemap. The Tilemap is a system of components which handles Tile assets and allows you to place them in your scene.
To create a new Tilemap, go to Create > 2D Object > Tilemap. This will also generate a Grid object, and your Tilemap will be a child of this object. (You can create more than one Tilemap, each will become a child of this Grid). This parent object has a Grid component within which you can set the size, spacing, and orientation of the Grid.
You will see this grid reflected in the Scene window…
The Tilemap object will contain the Tilemap Component and the Tilemap Renderer component. The most important setting to notice here is the Sorting Layer setting in the Tilemap Renderer. For our game, we will set up multiple Tilemaps under the same grid, and set them to different sorting layers to create foreground and background objects.
Once your Tilemap is created, you will want to create a Tile Palette – a collection of tiled sprites that you can use to populate the grid cells on your Tilemap. Access this by going to Window > 2D > Tile Palette. Once here, select “Create New Palette…” and give it a location and a name. Then you can drag sprites into this window which will create Tiles for each sprite. (You will see a window asking where you want to save these in your Asset directory. I recommend creating a new folder, as these can get pretty numerous if you are working with even a modestly sized sprite sheet.)
The Tile Palette window features a list of tools at the top. These are:
- Select – allows you to select one or more grid cells
- Move – allows you to relocate a tile (you must first select the tile)
- Paint – select a palette tile or tiles and use this to paint that selection onto your scene’s tilemap.
- Box Fill – fill an area of your tilemap with the selected tile(s) from your palette
- Pick – click on a tile to change your active brush selection to that type of tile
- Erase – click on a tile to remove it
- Flood Fill – fill a large area with the active tile(s)
Below the tile tools is the Active Tilemap setting. If you have multiple tilemaps (which is common with layering) you will want to make sure that you have the correct Tilemap selected before you paint. For this reason, it is recommended to give each of your Tilemaps a unique name that identifies their purpose (such as Foreground, Background, etc)
SideScrollerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(CapsuleCollider2D))]
/*
- SideScrollerController – created for UGE Summer 23
- Based on Sharp Coder 2D Controller (https://sharpcoderblog.com/blog/2d-platformer-character-controller)
*/
public class SideScrollerController : MonoBehaviour
{
// Player Movement Variables
public float maxSpeed = 3.4f;
public float jumpHeight = 6.5f;
public float gravityScale = 1.5f;
float moveDirection = 0;
public bool isGrounded = true;
public bool facingRight = true;
public LayerMask groundLayer; public Transform groundCheckObject; public float colliderRadius = 0.5f; // components Rigidbody2D r2d; CapsuleCollider2D mainCollider; // Transform t; // Start is called before the first frame update void Start() { // set the component variables r2d = GetComponent<Rigidbody2D>(); mainCollider = GetComponent<CapsuleCollider2D>(); // setting some component properties r2d.freezeRotation = true; r2d.collisionDetectionMode = CollisionDetectionMode2D.Continuous; r2d.gravityScale = gravityScale; // set the initial facing right value facingRight = transform.localScale.x > 0; } // Update is called once per frame void Update() { // Movement controls moveDirection = Input.GetAxisRaw("Horizontal"); // TO-DO: Implement Change Facing Direction if (moveDirection > 0 && !facingRight) { facingRight = true; transform.localScale = new Vector3(Mathf.Abs(transform.localScale.x), transform.localScale.y, transform.localScale.z); } if (moveDirection < 0 && facingRight) { facingRight = false; transform.localScale = new Vector3(-Mathf.Abs(transform.localScale.x), transform.localScale.y, transform.localScale.z); } // Jumping if (Input.GetButtonDown("Jump") && isGrounded){ r2d.velocity = new Vector2(r2d.velocity.x, jumpHeight); } } void FixedUpdate() { // Apply movement velocity r2d.velocity = new Vector2((moveDirection) * maxSpeed, r2d.velocity.y); // Check if player is grounded Collider2D[] colliders = Physics2D.OverlapCircleAll(groundCheckObject.position, colliderRadius, groundLayer); // check for a grounding collider isGrounded = colliders.Length > 0 ? true : false; } private void OnDrawGizmos() { // Draw a yellow sphere at the ground check position Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(groundCheckObject.position, colliderRadius); }
}