Level Scripting in Max Payne

What follows is an outline of a level from start to finish. Most of the work (including the write-up) took a day, but I’m still fine tuning it.  While I’m using MaxED, the Max Payne level editor, the basic ideas and approach are quite general and will work in any action-adventure game focused around melee combat.

Level name: Defence Training

Overview: Simple encounter against an increasing number of enemies in a martial arts dojo, with a catch – 1 hit kills the player.

Design Purpose: This optional challenge map is designed to encourage players to practise fighting defensively against multiple melee attackers.  The individual enemies are easy to defeat, but unlike the main game, one hit kills the player.  Inspired by the principal of Darwinian Difficulty and Hotline Miami, as well as the challenge maps in the Batman Arkham games, my reasoning is that allowing players to practise in an extreme (but fair) scenario should help them refine their fighting ability to the point where regular encounters in the story mode are trivial.

In particular, this level is designed to raise the player’s awareness of actions/moves which have invincibility frames – both defensive sidesteps, twists, and rolls, and special attacks. Controlling the positioning and movement during a fight is also encouraged (i.e. don’t get backed into a corner).

While called ‘defence’ training, there is a strong emphasis on interception — those who wait for their attacker to hit them; dodge or guard the hit; and then counter; risk getting hit (especially when attacked from all sides by multiple attackers). Player should quickly realise that survival relies on a balance between defence and offence; as well as range, timing, movement, prioritisation; and positioning. In other words – hit attackers before they have the chance to attack you; move defensively to avoid being swarmed; position yourself so that you can attack without being hit; time your attacks, so that you are not left vulnerable; prioritise which adversaries to take down first.

Lastly, efficient use of Bullet Time is encouraged to slow down time to concentrate on defensive movement and attack.

An additional purpose is to expose any flaws or bugs in the combat system. Testing under the hardest difficulty possible should highlight problems with difficulty, fairness, balance and flow (unexpected, random deaths; hitbox tuning; damage per move).

Design approach: I’m reusing the Dojo level for speed and simplicity – it’s a small-medium sized arena, suitable for combat with a small number of enemies (guessing 13 – 21 before it gets crowded/laggy).

The fight is broken down into several encounters, focusing around an enemy type; and each encounter will be broken down into 5 waves. Each wave will throw an increasing number of enemies at the player, in accordance with the Fibonacci sequence (1,2,3,5,8,13…). First encounter will start off with very slow and easy enemies; then we will introduce faster and tougher enemies; enemies with weapons (possibly including firearms); and finally mixed sets of enemy types. Between encounters might be a bonus/challenge wave.

Example:

Encounter 1

  1. 1 White dragon enemy, hostile when approached, very slow and weak
  2. 2 enemies
  3. 3 enemies
  4. 5 enemies
  5. 8 enemies

Encounter 2 – Same as 1, but with Yellow Dragon, an enemy which sprints at the player.

Encounter 3 – Mixed white and yellow; introduce red dragon (tank) 1. 1 red 2. 1 yellow, 2 white 3. 2 yellow, 3 white 4. 3 yellow, 5 white 5. 1 red, 3 white, 2 yellow?

For the sake of keeping things simple, I’m just going to focus on the first encounter.

Implementation

Encounter1

Fig. 1

Encounter 1 looks like this. Blue spheres are the actual NPCs, green spheres are the custom FSM scripts I use to control them. Note the first guy is missing because he starts in the level itself.  To preserve name-spacing, I should have placed him here and teleported him into the playing area when the level starts, but I don’t think this level is big enough to warrant that level of discipline.

An annoying facet of Max Payne level design is that NPCs cannot be spawned into a level, like they can in Unreal, for example. This means you have to frontload all of the enemies you want to use, and usually hide them in a room off the level somewhere. In many ways, this is actually a good thing because it forces design and coding discipline — you can’t afford to spawn hundreds of guys in a room (which is problematic design anyway), because it results in a huge scripting overhead. In levels where I want a lot of guys on screen, it’s a good idea to constrain yourself to the Fibonacci sequence, and organise encounters into waves to keep things sane, and to control pacing.

Another approach is to simply re-use NPC assets. You can see above that the maximum number of white dragon enemies the player will ever encounter in this level is 8. Hypothetically, it makes sense to simply load 8 of these guys, and simply respawn them on each wave (respawn meaning to resurrect to full health and then teleport to the desired location). I would do this, but honestly NPC necromancy can be problematic in Max Payne — especially with more complicated scripts. Usually it works, but sometimes it results in the enemies doing strange and unpredictable things when they respawn like standing in a dazed trance, playing an incorrect animation, or dying immediately on spawn. It also makes counting kills in waves much more complicated than it needs to be, because you have to work out a way for the level to know exactly which wave this enemy belongs to at any given time. Finally, re-using enemies results in bodies disappearing during combat. The absolute worst thing is for an NPC’s body to suddenly vanish moments after you just KO’d him.

I mentioned custom FSMs earlier – those are the green spheres. I had hoped that this level would be simple enough not to require individual FSMs per enemy, but the nature of the wave system requires me to count each time an enemy dies, so that the next wave is started when all enemies are dead. A quirk of our mod is that it’s possible to ‘kill’ NPCs multiple times, during what we call a ‘juggle’ combo. One frame before the player’s punch or kick lands, we spawn a ‘juggle’ area projectile which heals the NPC briefly (dealing -0.01 damage), which resurrects knocked out ‘dead’ enemies just in time for the kick to KO them again. This allows the player to ‘juggle’ defeated enemies, and perform stylish combos to finish them, which is both intrinsically satisfying and serves a gameplay purpose — you gain health back for juggling enemies.

An individual FSM is therefore required to count the first (and only the first) time the player ‘kills’ an enemy, otherwise juggle hits are counted, and the whole script breaks down. It’s for this specific reason that variants on the Kung Fu mod (including Katana) break the level scripts in vanilla Max Payne — especially the Jack Lupino fight in Ragnarock. The original Max Payne levels are not scripted to take into account juggle combos, and therefore scripted events can be triggered prematurely. This is why you have to be careful resurrecting NPCs.

Fortunately, this is fairly simple to implement in MaxED. NPCs have two primary states – Alive (default) and Dead. When the enemy is killed for the first time, they switch to the dead state, and a message is sent to the Wave script to increase the kill count by +1. If they’re killed again (by juggling), it doesn’t count because no further messages are sent to the wave script while the enemy is in the ‘dead’ state.

states

messages

In the story levels, regular melee enemies have between 3- 8 states (called ‘lives’ to distinguish them from HP), which allows them to get back up after being knocked down. This system originated from Max Payne: Katana, where I wanted to simulate combat by having enemies block a certain number of attacks before being defeated. Each enemy had a pre-set number of ‘lives’ and tougher enemies, such as bosses could withstand as many as 20+ hits before dying.

You make this system more interesting by having enemies regenerate lives over time, forcing the player to focus their attacks on a single enemy; or by layering this system with others (bosses in TRW have a regenerating ‘guard’ or shield which has to be ‘broken’ first before you can do permanent damage).  The exact implementation in TRW is much more complicated, because we wanted the enemies to do more interesting things, like get off the ground if they get knocked over, and allow the player to kick them into walls, or punt them across the floor like in the film; but I’ll save that for another post.

Even on modern systems, Max Payne can grind the framerate down if you’re sloppy, so it’s generally a good idea to remove NPC bodies at regular intervals — especially if you’re scripting an arena type level. For this map, enemies are removed every other wave to reduce the likelihood of the player noticing them disappearing.  In my experience, most people are too focused on fighting off adversaries to notice downed enemies being removed.  For best results, create a timer for every enemy and randomise the time between dying and removing the body, but that requires considerable overhead.

Zoning

Although the Dojo level is fairly small, it’s good design practise to divide your levels up into modules, or zones, and track where the player is. This allows you to control stress and tension by spawning enemies intelligently, relative to the player’s location.

As the Dojo is a square shaped arena, this is straightforward – I simply divide the level into thirds horizontally and vertically, giving me 9 zones. I could have divided into four quadrants, which is simpler, but offers limited tracking – especially if the player stays in the centre of the ring. With 9 zones, no matter where the player is, I can easily surround and envelop them by spawning enemies in the adjacent zones. 9 zones is also convenient because, if you remember, the maximum number of enemies I want to spawn (for now) is 8. So if I wanted to, I could spawn one enemy in each zone, leaving one space for the player. As you can see, working with the golden ratio naturally leads to more elegant designs.

Zones

For this to be manageable, the naming scheme for the zones has to be straightforward and meaningful; so I’ve gone with Centre, North, South, East, West, NE, NW, SE, SW.

This is fairly straightforward to implement in MaxED – you have a tracking FSM called Zoning with your 9 states (North, East, South,…). You then create 9 player collision triggers evenly spaced across the map to cover each zone, and on the activation of each trigger, you simply switch the state of zoning to whichever trigger was activated. So if the player activates the trigger ‘North’ the state of zoning switches to North. Although this is fairly simple, I recommend printing a direct message to the HUD to display which zone you’re in for initial testing — you don’t want to go to the effort of creating a complicated zoning system, if the actual tracking isn’t working correctly.

Another consideration is the fact that in MaxED triggers are spherical, which means that there will be some gaps between triggers. You can fix this by nudging the corner triggers (NE, NW, SE, SW) towards the centre a bit, or by simply increasing their radius slightly, along with the central trigger.

Zoning implementation

Fig. 5 An FSM tracks where the player is in the level, so we can spawn enemies in smart locations.

As for the actual waypoints used for spawning the enemies, originally I thought about using 3 per zone. However, this leads to a total of 27 waypoints, which is far too many. In the end, I settled for a total of 16.  That leaves me with 3 in the centre, 2 in each corner, and 1 for every point of the compass. This means I can spawn as many as 18 enemies at once, if I wanted to (which I don’t, as my budget is 8 npcs)

The final step is to write the code to actually spawn enemies. For every wave, there are nine possible zones where the player could be, so it’s a case of working out where the best location to spawn enemies is (i.e. the adjacent zones).  So that means there are 45 possibilities in total.

This might sound like a lot of unnecessary work for simply spawning enemies in, but in practise it doesn’t take that long to implement, and (aside from the challenge of building it) I think the results are definitely worth it. It’s also fairly scalable – assuming we don’t increase the number of enemies on-screen from 8, most of the work can be duplicated for additional encounters with minor modifications to the scripts.

Now we have a good foundation for this level.  It might not be apparent in the video, but the zoning system allows for a fairly natural ‘feel’ to the encounter – enemies spawn close to the action, not randomly; and the waypoints and timing can be tweaked to adjust stress, tension and flow.  Although basic enemies work well for this specific level (remember 1-hit kills, so anything complicated will overwhelm the player), were this an encounter in a ‘proper’ level, the next logical step would be to increase variety, by adding enemies of different class and role, as explained in this excellent and authoritative article on the topic. At any rate, this level is now the perfect template for testing all manner of encounters.

Advertisements