A guard that only checks how close you are is not a stealth game — it is a proximity alarm. It sees through walls and out of the back of its own head, and there is nothing to outsmart. A guard worth sneaking past checks three things: how far, which way it is looking, and whether anything is in the way. This guide builds all three.
The third check reads the level itself, so the walls need to be a tile layer. If you have not built one, start with how to use tile maps.
The three checks
All three must pass before a guard reacts. Order them cheapest first, so most frames stop at the first check:
- Range — is the player close enough to be seen at all?
- Facing — is the player in front of the guard, inside its cone?
- Line of sight — is there a wall between them?
Step 1: a guard reads the player
A guard cannot check anything until it knows where the player is. Max2D has expression readers that take an object id and give you that object's position, so a guard can use the player's coordinates directly in any expression.
Find the player object's id and keep it consistent. Every guard uses the same one.
Now the distance is a single expression — the distance maths function, given the guard's own position and the player's. Store it in a variable called d.
Step 2: local variables, or your guards will fight
Do that with a local variable, not a global one. This is worth stopping on, because it is the thing that breaks multi-enemy projects.
A global is shared by everything. If four guards each write their own distance into one global d, they overwrite each other within a single frame, and every guard ends up reading some other guard's number. The symptoms are baffling — guards reacting to nothing, or all reacting at once.
A local variable belongs to the object whose script it lives on. Four guards, four private copies, no interference. Every working number a guard uses — its distance, its facing, its sight samples — should be local. Only the shared alert level is global.
Step 3: the cone
Now, is the player in front? The clean way is to compare the direction to the player with the direction the guard is facing.
Keep it simple by having guards patrol along one axis, so a guard is facing left, right, up or down — a direction that is just +1 or -1. Then:
- Take the difference between the player's position and the guard's, along the axis the guard patrols.
- Multiply by the guard's direction, and divide by the distance
d.
That gives a number from -1 (directly behind) through 0 (directly to the side) to 1 (directly ahead). Pick a threshold — around 0.58 gives a cone of roughly 55° each side, which is a good, readable field of view. Above the threshold, the player is in the cone.
Draw the cone as four images — facing left, right, up and down — and swap them with Set sprite when the guard turns around. It is easier than rotating, impossible to get backwards, and you only change the sprite on the frame the guard actually flips rather than sixty times a second.
Step 4: line of sight, the one that matters
This is what turns a proximity alarm into a stealth game, and it is simpler than it sounds.
Take several points spread evenly along the line from the guard to the player, and use Get tile at each one against your wall layer. Get tile stores -1 where a cell is empty. So:
- Every sample below zero → nothing in the way → the guard can see.
- Any sample on a wall → blocked.
Each sample is one Get tile block with world coordinates: the guard's position, plus a fraction of the way to the player. Six samples, evenly spaced.
Six is not an arbitrary number. The two ways this goes wrong are not equally bad. If you use too few samples, a guard can see straight through a wall — the player deliberately takes cover and gets spotted anyway, which reads as the game being broken. If you use too many, a guard occasionally fails to spot a player it geometrically could, which players just read as getting away with it. Six was measured across every position in the example level as the cheapest setting where a guard never once sees through a wall.
Step 5: an alert meter, not an instant fail
When all three checks pass, add to a shared alert number — and have the player's script subtract a smaller amount every frame.
That one asymmetry gives you a proper stealth feel:
- Being seen for a moment is survivable.
- Being seen for a second is not.
- Breaking line of sight visibly cools it down, so the player can see the plan working.
Clamp it between 0 and 100 and show it with a Life bar component. At 100, restart. An instant game-over the moment a cone touches you is much less interesting — the meter is the entire tension of the genre.
Step 6: make the walls hide the cones
One last thing, and it is a rendering detail with real consequences.
The cone is a sprite — a big wedge of light. If the guards are drawn on top of the wall layer, that wedge paints straight over the walls, so the player sees a cone reaching through a wall that the guard's actual line of sight is correctly stopping at.
Give the wall layer a higher priority than the guards. Then the walls cover the cones, light stops where sight stops, and everything on screen tells the truth. A stealth game where you cannot trust what you see is not a stealth game.
Try it: Sightline
The example is a one-screen heist: four patrolling guards, five shards to collect, and a door that opens when you have them all. The whole level fits on the screen, so you can see every cone at once and plan a route.
Open any guard and read the script — it is the three checks in order, with a note beside them. Things worth changing: the sight range, the cone threshold (0.58), how fast the alert rises against how fast it drains, and the guard speed. Widen the cone to 0.2 and the level becomes almost impossible; that is a good way to feel how much the threshold matters.
Download Sightline.zip (97 KB)
In Max2D: Projects → Import Project → pick the zip. Play it in landscape.
Frequently asked questions
How do I make an enemy that can see the player?
Three checks, in this order, and all three must pass. Range: how far away is the player? Facing: is the player in front of the guard rather than behind it? Line of sight: is there a wall in between? Most enemies in beginner projects only do the first, which is why they seem to see through walls and out of the backs of their heads.
How does one object read another object's position?
There are expression readers that take an object's id and give you that object's position. Put the player's id in, and a guard can use the player's x and y in any expression — distances, directions, anything. It is the key to enemies that react to the player at all, and it means you do not need to copy the player's position into a global variable every frame.
How do I make walls block an enemy's vision?
Sample along the line between them. Take several points spread evenly from the guard to the player, and use Get tile at each one against your wall layer. Get tile stores -1 where the map is empty, so if every sample comes back below zero the view is clear; if any sample lands on a wall, the guard cannot see. Six samples is a good number — three is not enough and lets guards see through thin walls.
How many line-of-sight samples do I need?
Six, evenly spaced. It is worth being precise here because the two failure modes are not equal. Too few samples and a guard sees through a wall — the player takes cover and is spotted anyway, which feels broken and unfair. Too many and the guard occasionally fails to see a player it geometrically could, which players simply read as luck. Six evenly spaced samples was measured across every position in the example level as the cheapest setting that never sees through a wall.
Why do my enemies all behave identically or interfere with each other?
Because they are sharing global variables. If four guards all write their distance into one global called dist, they overwrite each other within the same frame and none of them is reading its own number. Use local variables instead: a local belongs to the object whose script it is on, so every guard gets its own copy and four guards can think independently in the same frame. This is the single most important thing to know when you have more than one of any kind of enemy.
Why does my vision cone show through walls?
That is a drawing order problem, not a logic one. The cone is a sprite, and if the guard is drawn after the wall layer the cone paints over the top of it. Give the wall layer a higher priority than the guards so walls cover the cones. It matters more than it sounds: if the light visibly spills through a wall while the guard's actual sight is blocked there, the player cannot trust what they are seeing and the whole game stops making sense.
How do I make an alert meter instead of instant game over?
Use one shared number. Each guard that can currently see the player adds to it every frame; the player's own script subtracts a smaller amount every frame. Being seen briefly is survivable, being seen for a second is not, and stepping out of sight visibly cools things down. Clamp it between zero and one hundred and show it with a Life Bar component. It is far better than an instant fail, because the player can see how much trouble they are in.