Doors have long been battled by gamedevs. While they seem like a simple static object in our day-to-day lives, in games, they are thresholds of pain.
- For pathfinding, they are a point where units must converge onto a single entryway.
- For traversing levels, they require loading of assets (famous half-life 2 door crashes).
- For AI, it is a common map traversal that requires a coordinated task.
- For collision, is it a kinematic obstacle, a dynamic one? How to prevent stuck states?
- The classic “Door Problem.”
So, how will Skymagi doors function?
Coding our doors
Let’s establish the baseline requirements for our doors:
- Ally units should open/close doors freely and pass through.
- Enemy units are blocked by closed doors and can attack them.
- Doors block line of sight (LoS) and lighting for allies and enemies.
- Doors should not block pathfinding or be static on our navmesh.
- Doors should not have obstacle collision avoidance for pathfollowing.
- Doors have a state (open, close, destroyed) that affects their collision body for all of the above.
Yeah, doors are nasty.
What if allied units didn’t collide with doors?
My first idea was to selectively disable door collision for all allies via a box2d mask, it would simplify a few collision differences. Doors would become purely flavor for allies, similar to Witcher 3’s implementation.
Pros:
- “Open” for allies is just an animation without an open/closed state.
- Simple to implement.
- No stuck state or weird collision when your units walk through doors.
Cons:
- Druid spells like entangle block doors for all, which would require changing a collision flag.
- Would still need open/close for enemy, or if player manually opened the door.
- Still need a sensor trigger for playing the animation.
Gameplay implications:
- No light floods the room when an ally opens their door, so enemies cannot see into it briefly.
- No strategy to bully past a door when an enemy opens theirs.
What if allied units did collide with doors?
This approach treats doors as standard kinematic bodies, but with a collision sensor that sets the door into an open state when it detects a collision with an allied unit. The open state disables the kinematic body collision.
Pros:
- There’s a simple open or closed state for doors.
- Clear LoS blocking without introducing new code.
Cons:
- Seems more difficult to implement with a contact listener and changing collision on the fly.
- Could result in an awkward logic with multiple units colliding with the door.
- Resetting back to close state may be finnicky.
Gameplay implications:
- Light would flood the room when opened, revealing into the interior for a moment.
- Strategy to bully past a door when an enemy opens it.
I chose this approach because I preferred its gameplay implications. Light spreading into a room and opening a door being a risk both seemed like fun elements in a close-quarters tactical game.
Implementation
I added a DoorSystem
and Door
component. Though typically with ECS, it is not a good practice to
create a component to represent an object, I realized that Door
is closer to a new behavior than
a type of object. A Door
adds an open and closed state to an entity and toggles its collision.
It can be reused for gates and other obstacles that might be used in towns and caves.
The door entity is a kinematic body, which is excluded from my NavMesh
builder naturally. I did have to add an
.exclude(Door)
to the MovementSystem
pathfollowing for collision avoidance, however.
First, I implemented manually toggling doors open, since the player castle has magical enchanted doors that they can explicitly open/close to deal with flooding, fires, and gas spells.
DoorSystem
listened to click events that collided with allied doors and set the state to open or closed.
Autodoors
Automatically opening/closing doors for allies was trickier.
DoorSystem
checked every entity with Door
and CollisionEvent
. When there
was a new collision, I checked the Faction
to determine if it was an ally, and attempted
to set the door state to open. When the collision event ended, I set it to close.
However, changing the kinematic fixture to a sensor and back was janky. I decided to have two box2d fixtures on doors. An always-available sensor that handled autodoor logic and a kinematic fixture that would be disabled based on state.
This approach worked for a single mage, but with more than one, the end collision event could trigger for the first and cause the other mage to have the door slammed in their face.
The fix was simple, my CollisionEvent
had an ongoing
collision array. So the DoorSystem
just had to ensure there were not any valid allied units actively colliding before allowing
the door to close.
// in tryCloseDoor, for each end collision event on a door
for (Entity blocking : collisions.ongoing) {
// check if any door-opener units are blocking us from closing
if (entityCanOpenDoor(requestedDoor, blocking)) {
return false;
}
}
And with that, basic doors are working!
You can still slam the door in front of enemies, of course.