
Pixel Adventure: Creating a Clone of Lemmings in Unity
- Transfer

Introduction
I think I was not the only one in my childhood playing Amiga at Lemmings. Decades passed, and I became, among other things, a game developer, a YouTube channel hosted with Unity tutorials .
One evening I stumbled upon these two videos ( Part 1 , Part 2 ) of Mike Dally about recreating Lemmings using Game Maker 2. I got nostalgia and I decided to do something with it. So I started creating my own version in Unity using my own resources (for obvious reasons).
In the article I will talk about my work process. However, to be brief, I will consider only the most important aspects. If it seems to you that this is not enough, then you can watch the video heredescribing the complete development process, line by line.
In addition, here you can play a project on WebGL. Possible bugs.
The complexity of the project was to recreate the sensations and mechanics of Lemmings. Including providing pixel-perfect collisions when moving across the level of many characters, which can vary depending on your skills.
Map creation
From the very beginning it was obvious that the level should be a grid that can be edited. The problem was to render this grid with each individual node, while maintaining a high game speed.
Gradually, I came to the decision to use one texture for the entire level, in which each pixel will be one node, and changes to the nodes will be a change in the texture itself.
To do this, we need to know that when changing the texture from the disk, Unity changes the texture itself, and not its instance. Therefore, we need to manually create this instance. This is very simple with the following code:
textureInstance = Instantiate(levelTexture) as Texture2D;
However, we need not only to create an instance of the texture, but also to set the nodes based on the color information received from the texture. Therefore, we will create a small Node class:
public class Node
{
public int x;
public int y;
public bool isEmpty;
}
Later we will be able to store a little more information in this class, but for now this is enough. Now, using the following loop, we can create a grid from this class:
//maxX - ширина текстуры, значение можно получить непосредственно из Texture2D
//maxY - высота текстуры
for (int x = 0; x < maxX; x++)
{
for (int y = 0; y < maxY; y++)
{
//Задаём новый узел
Node n = new Node();
n.x = x;
n.y = y;
//и затем задаём каждый адрес
//здесь мы попиксельно считываем текущую текстуру
Color c = levelTexture.GetPixel(x, y);
//затем задаём цвет экземпляра текстуры
textureInstance.SetPixel(x, y, c);
//здесь мы создаём информацию из текстуры; если это полностью прозрачная текстура, цвет пикселя будет иметь 0 в альфа-канале. Таким образом мы можем сказать, что этот узел пустой.
n.isEmpty = (c.a == 0);
//Здесь мы делаем то же самое, но на этот раз для миникарты. Все прозрачные пиксели рендерятся как зелёные
Color mC = minimapColor;
if (n.isEmpty)
mC.a = 0;
miniMapInstance.SetPixel(x, y, mC);
//и наконец мы назначаем узел нашей сетке
grid[x, y] = n;
}
}
//После цикла for нам также нужно выполнить для наших текстур .Apply()
textureInstance.Apply();
miniMapInstance.Apply();
Note: it is not necessary to set pixels for each Instance texture, as we do here, creating an instance before the loop will work, too, but we can use this technique to influence the pixels and give them a different color than usual. For example, in the case of an opaque pixel, you can replace its usual color with green. This way we can create a minimap.
Note: the for loop above should only be run once. Therefore, even for large textures (I use a texture of size 1000x200), excessive resource consumption will be only at boot time. After that, to make changes to the map, we will use the address stored in the node.
Now we have a texture and we need to render it. We add a GameObject with a SpriteRenderer (stored in the code below as levelRenderer) and convert our Texture2D to a sprite to assign it. This can be done using the following lines:
Rect rect = new Rect(0, 0, maxX, maxY);
levelRenderer.sprite = Sprite.Create(textureInstance, rect, Vector2.zero,100,0, SpriteMeshType.FullRect);
You can do the same for the minimap, but instead of Sprite Renderer, I used the Image UI component.
Vector2.zero is the center point, position 0,0 in the lower left corner. The value 100 next to it is the ratio of pixel to point, by default in Unity 1 point per 100 pixels. Also, when performing any calculations with world coordinates, it is important to know that, for example, to find the position in the world of the node node (5,6), we multiply x and y by the ratio, i.e. (x * (1/100)). Or you can set the ratio 1: 1 / for all sprites in the import settings
Finally, it is important that the sprite mesh is of type FullRect. Otherwise, Unity will optimize sprites, creating “islands” of opaque pixels. This will not be a problem when removing pixels from the map, but we need to add pixels to empty areas. By setting the type to FullRect, we will force Unity to store the sprite as a whole rectangle the size of the original image.

The figure above shows the problem that occurs when sprites are of type not FullRect.
Thanks to all of the above, we learned how to recreate a texture as a map.
Units and Path Finder

In this part I will skip the process of creating animations for units inside Unity, but if you do not know how they are created, then this process is disclosed in the video.
So how do we implement pixel-perfect collisions?
We already know where on our grid there are empty nodes and nodes of the earth. Let's decide on which nodes we can walk on. The "rule" of our game will be as follows: if the knot is the earth, and the knot above it is the air, then the top knot is the knot along which you can go. As a result, we will be able to walk from one empty node to another, but when we reach the node under which there is no land, we fall. Thus, creating a path search for each unit will be a relatively simple process.
We just need to follow these steps in the right order:
- Check if the current node is null. If this is true, then we probably fell off the map.
- Check if curNode is an output node, and if so, the unit has left the level (more on this below).
- Check if the bottom node is empty. This means that we are in the air, which means we are falling. If we fall more than four frames (if the last four nodes that we were moving along were empty), then we switch to the fall animation. Thanks to this, the animation does not change when we just go down the hill.
- If there is land beneath us, then we need to look forward. We look forward, if there is an empty node, then we move there.
- If the node in front is not empty, then we start looking up at four nodes until we find an empty node.
- If we did not find an empty node, then just turn and go in the opposite direction.
As you can see, this search for paths is very rudimentary, but for a game like Lemmings, it is more than enough.
There are other aspects that need to be considered when searching for paths, for example, the Umbrella ability, which is used when falling, or the Digging ability, when we dug very deep and there is no land beneath us, and so on.
That's the whole search for paths. To move units, we need to interpolate from one pixel to another. Or, you can add a certain number of points to the transform position at specified intervals, but with the help of interpolation we solve two problems. We limit the number of path search operations used so that they are not performed every frame, because units use path search only when they reach a pixel. Despite the fact that this is a simple operation, this way we can significantly save computing resources, thus increasing the number of units that can move at a level at the same time.
All units are controlled by a simple dispatcher who manually executes their Update (). Since Lemmings had an “accelerate” button, to recreate it we need to simulate timeScale time (when accelerated, it has a value of 2 or 3), which is transmitted to units along with a scaled version of the time delta. Units use scaled deltaTime (instead of Time.deltaTime) to interpolate between positions and use timeScale to change the speed in their Animator. Thus, the impression is that the game is in “fast mode”, although the Unity time scale remains at its usual speed.
Level change
And here the fun begins. We have a level, but the main part of Lemmings gameplay was the dynamic interaction with the levels.
In other words, we need to learn how to add or remove pixels. In both cases, we use the following code:
textureInstance.SetPixel(x,y,c);
Where c = color. The only difference between adding and removing a pixel is the alpha channel value. Do not forget. that the node is considered empty if the alpha value is 0.
However, as we recall from the above, when we use .SetPixel (), we also need to call .Apply () on the texture so that it really updates. We do not need to do this every time we change the pixel, because we can change several pixels per frame. Therefore, we avoid using .Apply () until the end of the frame. Therefore, at the end of our Update () loop, we have a simple Boolean value. When it is true, for both textureInstance and minimap, .Apply () is executed:
if(applyTexture)
{
applyTexture = false;
textureInstance.Apply();
miniMapInstance.Apply();
}
Abilities
After creating this system, we just need to determine which pixels we are affecting and how. In the article, I will consider only the high-level logic of events, but you can see the code in the video. Hereinafter, when I say "whether there is a pixel or not", then I mean the emptiness of the node, because if you did not fall from the map, then there will always be a pixel in any place.
- Walking
- This is a basic ability. Moving on, there is nothing interesting.
- Stop
- Stop! We redirect units to the other side.
- Umbrella
- When a unit falls from a great height, he dies, but with this ability he smoothly descends, like Mary Poppins. This ability works very simply, it just checks its activity. In addition, it changes the fall rate for interpolation between pixels to create a relaxed fall effect.
- Digging forward
- In the original game, it was called “Basher” - the unit simply dug a tunnel forward. From the point of view of logic, it redefines the search for the path: if there is at least one pixel in front, then the unit continues to dig for a certain number of pixels, then for each pixel forward it takes 8 pixels above it and sends them so that they are deleted. The size of eight pixels is chosen due to the growth of our characters.
- Digging down
- It looks like digging forward. Instead of taking pixels in front and on top, the ability takes 2-3 pixels under the unit, 2-3 forwards, and, of course, from one row below.
- Blasting
- This is a simple suicide unit, it just explodes, leaving a hole in its place. The deleted pixels are at a given radius around its position.
- Construction
- Another classic ability - the unit builds a series of pixels diagonally upward from the point at which it is looking. This is achieved by obtaining 4-5 pixels in front and at the top with further transferring them to “add”. It also affects the properties of nodes (turns a node from empty to filled). After that, the ability interpolates to the next diagonal position and repeats the operation until it collides with the wall or until the number of pixels allocated for the building is over.
- Filling
- Another fun ability that smoothly brings us to the next section. It first appeared in Lemmings 2: it is a unit that throws pixels of “sand” that have the properties of a “liquid” - it always goes down until it reaches a quiescent point. We will consider them in more detail later.
Obviously, there are much more other abilities in the Lemmings series of games, but I believe (although I have not tested them all) that they will be just combinations of all of the above. Miner, for example, is a variation of Basher, but instead of moving forward, it digs forward and down.

Going down smoothly like Mary Poppins

Examples of using some abilities
"Liquids"
Or as I call them, fill nodes. This is another fun addition. Their high-level logic is as follows: these are dynamic nodes that have their own "path search". Put them all together and get a fluid effect. As shown in the video, you can also create the effect of falling snow.
Here's how it works - we have the following class:
public class FillNode
{
public int x;
public int y;
public int t;
}
x and y are, as you might guess, the node address. t here is the number of times the fill node was in the “dead end position”. You can experiment with numbers, but for myself I chose that if he was in it 15 times, then the node is considered to have stopped, thus turning from a filling node into a regular node.
Fill nodes are not updated every frame, because then they would have to change the position 60 times per second, because of which they could move too fast. We use a universal timer that acts only on them. Personally, I used the update once every 0.05 seconds, but you can experiment with it to get various fluid effects.
So, when they are updated, their “path search” is as follows:
- We check if the node exists under our node, if not, then this fill node fell outside the map.
- If it exists, then check if it is empty. If it is empty, then clear the pixel in which we are and add to the pixel (node) below. Thus we continue to move down.
- However, if it is not empty, check the pixel forward and down. If it is empty, move there.
- If forward-down is full, then move back-down.
- If we are in a position from which there is nowhere to move, then we add the filling node to t.
- If t is greater than the value we set, then delete the fill node and leave the pixel full.
t is responsible for three aspects. Firstly, by deleting nodes that are not moving, it ensures that we do not have a huge list of filling nodes. Secondly, it allows you to avoid sticking nodes to each other when falling. Thirdly, if the node is at an impasse, but has not yet been considered stopped, then the unit digging under it will cause the fill node to fall down during the next update, which creates a beautiful dynamic effect.
Given all of the above, you might think that the effect of the "fluid" greatly affects the speed, but in our case this is not so. As shown in the video, I tested it with large numbers (more than 10,000 fill nodes). Although there was no chance that all 10,000 would be “alive” at the same time, this created a very beautiful effect, shown below.

Play with this additional system. The “placeholder” ability only needs a spawn position, after which it will create several filling nodes and allow them to move independently.
Level editor
You probably guessed that it would not do without it, because in essence, all we did was change the colors of the pixels. Therefore, the next logical step will be to create a level editor, which, of course, will be a pixel drawing. I don’t think it’s worth going deeper into the analysis of this editor, because it only uses the same functions described above, but it focuses not on units, but on the position of the mouse. Of course, if you want to see the whole code, you can watch the video.
But I want to talk more about serialization, so ...
Serialization
As I showed in the video, there are several ways to implement it. I don’t even have to use an editor to create levels. We can just load the image into a .png from disk and use it as a level. The system already created by us opens up great opportunities. However, we still need to create a game from the texture, so we need to keep the spawn position and exit position (let's call them two events). There were several such events in the sequels of the original Lemmings, but let's focus on each of them, the basis of the logic will be the same.
Of course, there are many ways to implement these events, for example, creating two textures per level, one with the level itself, and the second with color-coded events, but in this case, players can change the level events in a third-party editor. This is not necessarily bad, but sometimes undesirable, for example, if you have campaign levels.
As a result, I decided to serialize everything I need on a single file level. The only thing worth mentioning here is, of course, it is impossible to serialize Texture2D directly, but you can convert it to an array of bytes by encoding the texture. Unity has simplified our lives because we just do the following:
byte[] levelTexture = textureInstance.EncodeToPng();
Advanced Level Editor
Although the level editor is a good feature of the game, most likely, players will be bored of starting from scratch every time. As mentioned above, in the video I already demonstrated a way to download .png from a file, but I wanted the game to work on WebGL without solving complex problems.
I came up with the following solution - to allow players to insert a link from the level editor, and then load it as an instance of the texture. After that, the question will only be to set level events and save.
How to do this? Unity has a WWW class, so we will create a coroutine for loading textures:
IEnumerator LoadTextureFromWWW(string url)
{
WWW www = new WWW(url);
yield return www;
//при загрузке www проверять, есть ли текстура
if(www.texture == null)
{
//ссылка недействительна
}
else
{
//у нас есть текстура, тогда
textureInstance = www.texture;
}
}
Note: although the above code is correct, it is still a pseudo-code, because there are several additional lines in the working code that process the UI, reinitialize the editor, etc. I have not added them here so that we can focus on the most important. Of course, the full code is in the video.
All the code written above works great in desktop builds, because you can paste a custom clipboard into your own Unity input fields. However…
Changes for WebGL assembly
... in WebGL this is not allowed. You can work around this problem by inserting a small javascript fragment into the index file of the WebGL assembly. Something like this:
//прямо под этой строкой (она уже присутствует в WebGL-сборке)
var gameInstance = UnityLoader.instantiate("gameContainer", "Build/lennys.json");
//добавляем
function GetUserInput(){
var inp = prompt("link");
gameInstance.SendMessage("GameManager","ReceiveLink",inp);
}
Thanks to this, a pop-up window appears in the browser with a text field where you can insert the link. After clicking on OK, the script will send a message, find the game object "GameManager" and the function "ReceiveLink", in the signature of which there is a string inp. The function itself looks like this:
public void ReceiveLink(string url)
{
linkField.text = url;
}
Here linkField is a UI InputField element, nothing special.
It is worth noting the following:
- Even though we have a script called GameManager, JavaScript is looking for a game object (not a class!) With that name. The function we are calling is located in the UIManager class (which is in the same gameobject).
- The texture does not load until the player clicks on the texture download button in the game UI
To execute the JavaScript function, you need to add the following lines when you click the url download button:
Application.ExternalEval("GetUserInput()");
In addition, there is another limitation in WebGL. Yes, you’ve already guessed - we can’t keep the user level without securing a headache. How to solve this problem? Very simple - set the MVP for our project.
However, we still need to let the players play the levels they created, downloaded as images or hand-drawn. Therefore, we will keep them on the fly. This means that all created levels will be lost in the next session. This is not a very serious problem, because you can upload images online. First, we determine whether we are on the WebGL platform, this is done as follows:
if (Application.platform == RuntimePlatform.WebGLPlayer)
isWebGl = true;
Note: all this I do mainly in the framework of the tutorial, in fact I planned to turn everything into WebGL, because I never intended to create assemblies for desktop computers or other platforms. Therefore, instead of saving files locally, we will store them in memory.
Conclusion

It was a very interesting project. I worked on it in my free time and it took 1-2 weeks, but the direct work took about the same time as in the video, except for the time to study and create “graphics” and “animations” of units. And of course, there are bugs in the game.
It's also worth noting that besides a few API calls, the code is basically very simple.
The pixel-perfect hike demonstrated here was also the starting point for creating several other games of that era. I won’t reveal secrets here, but I’ll soon work on other projects, follow my channel and support it if you like it.