I stumbled across a great resource awhile back at http://opengameart.org. You can find a large assortment of assets for game development including art and even music, and as the name implies, you are free to use most of it in your projects. This coupled with the enjoyment I had with the random world creator I made in the last post and I have decided to move away from the purely text based RPG. It will be easier than I thought to add some graphics, so in this post, I will show how you could extend the Procedural World Visualizer into a sprite based equivalent.
Prerequisites
If you haven’t already read my previous post, you should do so now. It includes and explains some source code you will need to complete this demo. I won’t be re-explaining any of that portion of the code, and will merely comment on how to use it to drive the sprites that get picked to layout a level.
You will also need art resources. The sprite pack I ended up choosing is found here Crawl Tiles – a 2.7 Mb zip file full of TONS of sprites for terrain, dungeons, decoration, monsters, and probably everything else you could hope to implement in our simple RPG project. Of course you could use any sprites you like or create your own.
Create the Scene
The first thing we need is a way to show our world on screen. We will do this by taking advantage of Unity’s Grid Layout Group component to show our sprite images.
- Create a new scene.
- Add a Panel. From the menubar, choose “GameObject->UI->Panel”. I named my panel “Scenery” and removed the default Image and Canvas Renderer components.
- Modify the Scenery Panel’s Rect Transform so that all anchor and pivot fields use a value of 0.5. Then zero out the position, and set the size to width:640 and height:480.
- Add the component, “Grid Layout Group” to the Panel. Set the “Cell Size” to 32 on both X and Y. Also set the “Start Corner” and “Child Alignment” properties to “Lower Left”.
- Create a new script called “PerlinSpriteVisualizer.cs” and add it to the panel.
- Create a new Image object. From the menubar, choose “GameObject->UI->Image”.
- Create a prefab from the new Image object by dragging it into the Project pane. Then delete the object from the scene. The prefab we just created will be assigned to a property reference on the PerlinSpriteVisualizer class in a moment.
- Select the parent canvas object. In the inspector, modify the Canvas Scaler’s “Ui Scale Mode” to “Scale With Screen Size”. Put in a “Reference Resolution” of “x:640 y:480”. Set the “Screen Match Mode” to “Shrink”. All of these settings will help to make sure that our interface stuff will scale to fill the camera screen regardless of the aspect ratio and resolution we actually end up using.
Get Some Sprites On Screen
We are going to need a class which can take an x and y position (the tile’s coordinates on screen) and determine what sprite should appear. This functionality is defined in the abstract base class:
public abstract class SpritePicker : MonoBehaviour { public abstract Sprite Pick (int x, int y); }
Our first few concrete subclasses are almost identical to the idea I used for the EnemySpawner post – one Fixed that always returns the same sprite, one Random that can return any sprite from an array at random, and one weighted, which is more likely to return certain sprites based on higher weights. If you find this or the topic of polymorphism confusing, you may want to go back and read that post.
public class FixedSpritePicker : SpritePicker { public Sprite sprite; public override Sprite Pick (int x, int y) { return sprite; } }
public class RandomSpritePicker : SpritePicker { public Sprite[] sprites; public bool persistant = true; public override Sprite Pick (int x, int y) { if (persistant) return PickPersistantRandom(x, y); else return PickRandom(x, y); } Sprite PickPersistantRandom (int x, int y) { // Store the seed that had been active int oldSeed = UnityEngine.Random.seed; // Seed the random generator based on the coordinates UnityEngine.Random.seed = (x + 3) ^ (y + 7); // Pick a sprite to return Sprite retValue = sprites[ UnityEngine.Random.Range(0, sprites.Length) ]; // Restore the original seed UnityEngine.Random.seed = oldSeed; return retValue; } Sprite PickRandom (int x, int y) { return sprites[ UnityEngine.Random.Range(0, sprites.Length) ]; } }
[System.Serializable] public class WeightedSprite { public Sprite sprite; public int weight; } public class WeightedRandomSpritePicker : SpritePicker { public WeightedSprite[] options; public bool persistant = true; int optionCount; int wheelSize; void Start () { BuildWheel(); } public void BuildWheel () { wheelSize = 0; optionCount = options.Length; for (int i = 0; i < optionCount; ++i) { wheelSize += options[i].weight; } } public override Sprite Pick (int x, int y) { if (persistant) return PickPersistantRandom(x, y); else return SpinWheel(); } Sprite PickPersistantRandom (int x, int y) { // Store the seed that had been active int oldSeed = UnityEngine.Random.seed; // Seed the random generator based on the coordinates UnityEngine.Random.seed = (x + 3) ^ (y + 7); // Pick a sprite to return Sprite retValue = SpinWheel(); // Restore the original seed UnityEngine.Random.seed = oldSeed; return retValue; } Sprite SpinWheel () { int roll = UnityEngine.Random.Range(0, wheelSize); int sum = 0; for (int i = 0; i < optionCount; ++i) { WeightedSprite option = options[i]; sum += option.weight; if (sum >= roll) return option.sprite; } return null; } }
Note that the Random and Weighted versions of the Sprite Picker both include a boolean indicating whether or not the selection is “persistant” which I default to true. Normally, every time you choose a random value, Unity returns a different value. However, because we are using the random value to pick a sprite to appear in our wold, we want to make sure that the sprite that was picked for any particular location is always the same. For example, imagine you have your character move right, scrolling the background by one tile. Then you move back to where you were. If the tiles which loaded randomly were not persistent, you might notice flowers appearing or disappearing in the grass.
The way that this randomness is persisted is by assignment of a “seed” value to the Random class. The value that Random actually returns is not actually random, but because the seed changes, the result we get back does. By specifying a seed, the value we generate will always be consistent.
Next implement the PerlinSpriteVisualizer script which we created earlier. Assign the Image prefab I had you create to the “tilePrefab” property.
public class PerlinSpriteVisualizer : MonoBehaviour { #region Properties public GameObject tilePrefab; public SpritePicker spritePicker; public Image[] tiles; public int xPos, yPos; int _w, _h; #endregion #region MonoBehaviour void Start () { Init (); Refresh(); } void Update () { bool dirty = false; if (Input.GetKeyUp(KeyCode.UpArrow)) { yPos++; dirty = true; } if (Input.GetKeyUp(KeyCode.DownArrow)) { yPos--; dirty = true; } if (Input.GetKeyUp(KeyCode.LeftArrow)) { xPos--; dirty = true; } if (Input.GetKeyUp(KeyCode.RightArrow)) { xPos++; dirty = true; } if (dirty) Refresh(); } #endregion #region Public public void Refresh () { for (int y = 0; y < _h; ++y) { for (int x = 0; x < _w; ++x) { int index = y * _w + x; tiles[index].sprite = spritePicker.Pick(x + xPos, y + yPos); } } } #endregion #region Private void Init () { RectTransform rt = GetComponent<RectTransform>(); GridLayoutGroup glg = GetComponent<GridLayoutGroup>(); _w = Mathf.FloorToInt(rt.rect.width / glg.cellSize.x); _h = Mathf.FloorToInt(rt.rect.height / glg.cellSize.y); int count = _w * _h; tiles = new Image[ count ]; for (int i = 0; i < count; ++i) tiles[i] = CreateTile(); } Image CreateTile () { GameObject instance = Instantiate(tilePrefab) as GameObject; instance.transform.SetParent(transform, false); return instance.GetComponent<Image>(); } #endregion }
Create an Empty GameObject in the scene called “Sprite Pickers”. This will be an object I use for organization in the scene hierarchy. Create another Empty GameObject in the scene called “Fixed Sprite Picker” and parent it to the “Sprite Pickers” object. Attach the “FixedSpritePicker” script as a component and assign any of your project’s sprites to its “Sprite” property – I used a tileable dirt sprite – “grass_full” (poorly named considering it is a picture of dirt) if you are using the same texture pack.
Assign the “Perlin Sprite Visualizer” script’s “Sprite Picker” property to the Fixed sprite picker you just created. If you run the scene now, you should see an array of those images fill the panel without any gaps.
Create two more objects in your scene for the “RandomSpritePicker” and “WeightedSpritePicker” classes just like we did for the “FixedSpritePicker”. Make sure to parent them to the “Sprite Pickers” object to help keep the scene organized. Find a group of sprites that are intended to be swappable, such as versions of grass with and without flowers, weeds, etc. and assign them to the other pickers. Then try running the scene with one of them set as the PerlinSpriteVisualizer’s SpritePicker. The ground should look a lot better when it picks from a variety of sprites over using the exact same tiled sprite. It will help to break up the obvious tiling of the images you are using.
With the scene in play mode, note that you can use the arrow keys to scroll the background. It almost feels like you are exploring a game world already!
Use Some Perlin Noise
Getting sprites on screen was sort of exciting, but I really want to see the Perlin noise examples from the previous post driving the layout of our sprites. Let’s do that now.
Create a new class called “Painter” that will function kind of like the PerlinNoiseVisualizer from the previous post. It takes an x and y position and returns a Color.
public abstract class Painter : MonoBehaviour { public abstract Color Render (int x, int y); }
The concrete implementation presented below works off of the RenderLayer implementation we used in the previous post.
public class RenderLayerPainter : Painter { public RenderLayer[] layers; public Color Render (int x, int y) { Color c = Color.black; for (int i = 0; i < layers.Length; ++i) { c += layers[i].Render(x, y); } c.r = Mathf.Clamp01(c.r); c.g = Mathf.Clamp01(c.g); c.b = Mathf.Clamp01(c.b); c.a = Mathf.Clamp01(c.a); return c; } }
You could create other concrete implementations of Painter, such as one that simply reads an image and passes along its colors. This way you could hand paint levels in photoshop, etc. At the moment that will be left as an exercise for the reader.
Now I will create a SpritePicker subclass which uses a painter to drive the sprite selection. Note that it maps pixel colors to other sprite pickers instead of directly to sprites. This allows a nice hierarchy of complexity, such as saying any white pixel will be dirt, and black will be grass, but if the pickers are implemented as RandomSpritePickers, then the white pixels will be random dirt sprites instead of a constantly repeated dirt sprite.
[Serializable] public class PaletteTheme { public Color color; public SpritePicker picker; } public class PaletteSpritePicker : SpritePicker { public PaletteTheme[] theme; public Painter painter; Dictionary<Color, SpritePicker> map = new Dictionary<Color, SpritePicker>(); void Start () { for (int i = theme.Length - 1; i >= 0; --i) map.Add(theme[i].color, theme[i].picker); } public override Sprite Pick (int x, int y) { Color c = painter.Render (x, y); SpritePicker sp = map.ContainsKey(c) ? map[c] : theme[0].picker; return sp.Pick(x, y); } }
Create another empty GameObject for our Palette Sprite Picker class. Name it and parent it as we have done before and assign it as the Sprite Picker for the Perlin Sprite Visualizer script. I set the Theme to have two entries, the first with a color of black (make sure the alpha is full white) and a picker set to the Random Sprite Picker we created earlier (to assign a random grass tile). The second theme element has a color of white and maps to the Fixed sprite picker (dirt).
Make another empty GameObject called “Painter” and add the “RenderLayerPainter” script. I set it up like the last example of the previous post where there are two Render Layers, the first driven by a “PerlinMapper” at a scale of 0.1 and using a “BarNode” with a value of 0.6. The second layer uses “PerlinMapper” at a scale of 0.15 and a “BandNode” with values of 0.4-0.6 set as the range. These scripts were all defined in the previous post in case you missed them. Assign this painter object to the painter property of the “PaletteSpritePicker” and then run the scene.
You should see a very nice perlin noise driven example. Feel free to explore the world using the arrow keys and notice how the pattern is unique for as long as you explore, but the same coordinates always show the same thing. You can use this fact to hand place certain elements like cities or dungeons if you were so inclined.
Add Transitions
Many tile engines use transition tiles between to ease the visual contrast between two types of environment tiles. The sprite bundle I downloaded has sprites for this purpose: grass_nw, grass_n, grass_ne, etc. It is not hard to create a sprite picker which can evaluate the position of a pixel and its surrounding pixels to determine which transition tile should be added.
We will create a new script called “TransitionSpritePicker” for this purpose.
[Flags] public enum TransitionEdge { None = 0, Top = 1 << 0, Bottom = 1 << 1, Right = 1 << 2, Left = 1 << 3 } public class TransitionSpritePicker : SpritePicker { public Painter painter; public SpritePicker none; public SpritePicker topLeft; public SpritePicker topMiddle; public SpritePicker topRight; public SpritePicker left; public SpritePicker full; public SpritePicker right; public SpritePicker bottomLeft; public SpritePicker bottomCenter; public SpritePicker bottomRight; Dictionary<TransitionEdge, SpritePicker> map = new Dictionary<TransitionEdge, SpritePicker>(); void Start () { map.Add(TransitionEdge.None, none); map.Add(TransitionEdge.Bottom | TransitionEdge.Right, topLeft); map.Add(TransitionEdge.Bottom | TransitionEdge.Left | TransitionEdge.Right, topMiddle); map.Add(TransitionEdge.Bottom | TransitionEdge.Left, topRight); map.Add(TransitionEdge.Top | TransitionEdge.Bottom | TransitionEdge.Right, left); map.Add(TransitionEdge.Top | TransitionEdge.Bottom | TransitionEdge.Left | TransitionEdge.Right, full); map.Add(TransitionEdge.Top | TransitionEdge.Bottom | TransitionEdge.Left, right); map.Add(TransitionEdge.Top | TransitionEdge.Right, bottomLeft); map.Add(TransitionEdge.Top | TransitionEdge.Left | TransitionEdge.Right, bottomCenter); map.Add(TransitionEdge.Top | TransitionEdge.Left, bottomRight); } public override Sprite Pick (int x, int y) { TransitionEdge edge = GetEdges(x, y); SpritePicker sp = map.ContainsKey(edge) ? map[edge] : none; return sp.Pick(x, y); } TransitionEdge GetEdges (int x, int y) { Color myColor = painter.Render (x, y); TransitionEdge edge = TransitionEdge.None; if (myColor == Color.black) return edge; if (painter.Render(x, y+1) == myColor) edge |= TransitionEdge.Top; if (painter.Render(x, y-1) == myColor) edge |= TransitionEdge.Bottom; if (painter.Render(x+1, y) == myColor) edge |= TransitionEdge.Right; if (painter.Render(x-1, y) == myColor) edge |= TransitionEdge.Left; return edge; } }
This example is not as efficient as it could be, because the painter it uses recalculates the color of a location every time it is queried instead of caching values. And in this case, every tile queries itself and the four tiles around it, so the total number of queries grows very quickly. When the math is quick enough, this is not a serious issue, but depending on the amount of tiles and the complexity of the noise, it could become one.
Create another SpritePicker container in your scene for the TransitionSpritePicker script. Use the same Painter we used for the PaletteSpritePicker. I created FixedSpritePickers for all of the transition tiles, and assigned them to the appropriate properties, but the “none” sprite picker was assigned to the random grass picker we already had created. Run the scene and you will see a version similar to the palette version, but with nice transitions – in most of the locations. The problem is that our painter is creating patterns where a transition is required that we didn’t define. For example, a tile that has matches at its top and bottom but neither side. I defaulted to the “none” picker so it shows grass in those locations, but you will probably notice them because the transition edge is completely sharp wherever they appear. Remedies for this issue could include creating the additional transition tile possibilities and adding them to the script, only using this picker on hand painted maps, or caching and evaluating the pattern in the painter to remove / repaint tiles that are not supported.