Actor replacement

From ZDoom Wiki
Jump to navigation Jump to search

"Actor replacement" is the concept/method often applied in gameplay modifications that define their own actors (such as weapons, items, monsters, etc.) but do not feature their own maps.

There are several ways to perform actor replacement in GZDoom, and a number of things to keep in mind.

The most important thing to keep in mind is that the term "replacement" does not imply a literal replacement of a class. There is, in fact, no way to replace one actor with another actor in ZScript. The methods outlined on this page only instruct the game to spawn one actor instead of the other. The original actor will still be loaded, will still exist in the game, and it will still be possible to make it spawn. As such, it's important to remember the following:

  • If you create a mod that replaces weapons with custom weapons but the player uses a cheat code like give all, they will get both your mod's weapons and the original weapons in their inventory. This applies to all types of items, not just weapons. Normally, they won't be able to select them, but they may appear in their HUD, and they may end up switching to them when they run out of ammo for a mod's weapon, unless all mod's weapons have low enough values for Weapon.SelectionOrder.
  • Player starting items (defined by the Player.StartItem property) do not spawn, they're placed in the player's inventory directly, and thus cannot be captured by replacement methods.
  • ACS scripts that utilize GiveInventory and TakeInventory functions with explicit class names are not capable of recognizing modded weapons/items.
  • The Actor.Spawn() function by default does not allow replacements, but it can be controlled by its third argument. This is especially relevant when creating addons for other mods.

When actor replacement is NOT needed

Spawning an actor for testing purposes

If you've defined a custom actor and just want to spawn it for testing purposes to see if it behaves correctly, you don't have to worry about creating a replacement. Simply use the summon actorname console command where actorname is a class name, like DoomImp. Class names are case-insensitive.

When you need to place actors in your custom maps

If a project comes with custom maps, custom actors can be placed in those maps directly, via a map editor. To do that, actors need to be given editor numbers (also known as DoomEdNums) with MAPINFO. You can also add editor keys to your actors in ZScript/DECORATE, in order to customize the appearance of their markers in the map editor (color, size, arrow, and so on).

In addition to that, if the actors are defined in ZScript, the file gzdoom.pk3 must be added as a resource in Ultimate Doom Builder. To do that, use Map Options (F2) > Add resource... > From PK3/PK7 > navigate to the GZDoom folder and pick gzdoom.pk3. You must also tick the "Exclude this resource from testing parameters" checkbox. If gzdoom.pk3 isn't added as a resource, actors defined in ZScript will not show up in Ultimate Doom Builder even if they have editor numbers defined properly.

Player classes

Player classes cannot be replaced directly, so none of the standard replacement methods apply to them. Code like class MyPlayer replaces DoomPlayer has no effect at all.

If you want to add a player class to your project, you need to create a new actor based either on PlayerPawn or one of the existing classes, like DoomPlayer, and then add it via MAPINFO. You will find details on this page: Creating new player classes.

Modifying actor properties on the fly

If you want to only add minor changes to actors, consider modifying existing actors dynamically instead of creating new actors to serve as replacements. For example, let's say you want to modify the PickupSound of all keys in Doom. Using an event handler you can do this much more easily than doing it via actor replacement:

// Runs whenever an actor spawns in the world:
override void WorldThingSpawned(worldEvent e)
{
	// Try casting the spawned actor as the Key class:
	let k = Key(e.thing);
	// If the cast succeeded, this means this actor
	// is based on the Key class, i.e. is a key:
	if (k)
	{
		// Modify its pickupsound directly:
		k.pickupsound = "pickups/keycard"; //(SNDINFO sound name)
	}
}

This code will modify the pickupsound of all actors based on the Key class (are more robust system that checks for specific key classes is, of course, possible too). This saves you the need to define multiple replacements, or worry about key-specific challenges, like lock numbers.

This approach can be applied to all dynamically modifiable actor fields (most of them are), and is very useful for purely cosmetic changes (like changing sounds, PickupMessage, and so on). It improves compatibility because a change like this does not prevent actors from being replaced by gameplay mods. It's also very efficient for mass changes (like in the example above, where the change will apply to all actors based on the Key class, instead of having to manually go through every existing key).

Injecting new behaviors into existing actors via controllers

ZScript offers a lot of tools to add or modify actor behavior without replacing the actor. This can be a very beneficial choice when creating mods that need to affect other actors (for example, add weapons that can affect monsters in specific ways), because this lets them be universally compatible.

Aside from modifying properties on spawn, as described above, you can also modify them temporarily with the help of controllers—separate classes that are attached to the actor and affect it in some way. Usually those controllers are custom Inventory items or dummy Powerups.

For example, say, you're creating a weapon that is supposed to set monsters on fire, and you want to do this for all monsters. While you could, of course, replace each and every monster and add a custom Pain state and activate it with a custom damagetype, it'd be much easier and more robust to handle it with a controller.

Here's an example of a controller based on the Powerup class that spawns particles around the actor (meant to be a monster) who has it, and also deals damage to them every second:

class ActorFireController : Powerup
{
	// This will store a pointer to the player who fired the original
	// projectile, so that the damage can be passed from this pointer
	// and the player gets proper kill credit:
	Actor fire_setter;

	// These colors will be used for the particles:
	static const color fireColors[] =
	{
		"fb8402",
		"bb0e00",
		"ffa103",
		"790d0b"
	};

	Default
	{
		Powerup.Duration -10;
	}

	override void DoEffect()
	{
		Super.DoEffect();
		// Null-check the owner just in case, and return
		// if it's null:
		if (!owner)
		{
			return;
		}

		// This block will spawn flame-like particles
		// around the burning enemy:
		FSpawnParticleParams fp;
		// Pick a random color from the fireColors static array:
		fp.color1 = fireColors[random(0,fireColors.Size()-1)];
		fp.lifetime = random(30, 40);
		fp.style = STYLE_Add;
		fp.flags = SPF_FULLBRIGHT|SPF_REPLACE;
		// Position the particles within the monster's hitbox:
		fp.pos.x = owner.pos.x + frandom(-owner.radius, owner.radius) * 0.6;
		fp.pos.y = owner.pos.y + frandom(-owner.radius, owner.radius) * 0.6;
		fp.pos.z = owner.pos.z + frandom(owner.height * 0.25, owner.height * 0.8);
		fp.startalpha = frandom(0.7, 1.0);
		fp.fadestep = -1;
		fp.size = frandom(8, 18);
		// size reduces to 0 over lifetime:
		fp.sizestep = -(fp.size / fp.lifetime);
		// the particles will be slightly accelerated horizontally
		// and pushed upwards:
		fp.vel.xy = (frandom[fp](-2, 2), frandom[fp](-2, 2));
		fp.vel.z = frandom[fp](2, 4);
		fp.accel.xy = -(fp.vel.xy * 0.035); //acceleration is aimed to the opposite of velocity
		fp.accel.z = -(fp.vel.z / fp.lifetime);
		fp.startRoll = frandom[fp](0, 360);
		fp.rollvel = frandom[fp](-15, 15);
		fp.rollacc = -(fp.rollvel / fp.lifetime); //rollvel reduces to 0 over lifetime
		Level.SpawnParticle(fp);

		// Every second while the owner is alive and the fire_setter pointer is not null,
		// the monster will receive damage:
		if (owner.GetAge() % TICRATE == 0 && fire_setter)
		{
			// fire_setter is passed as the source of the damage:
			owner.DamageMobj(fire_setter, fire_setter, 5, 'Fire', DMG_THRUSTLESS);
		}
	}
}

(See SpawnParticle and the modulo operator in ZScript for extra information)

And here's how you could make your projectiles give this controller to monsters hit by them. In this example the projectile is based on DoomImpBall, but you can inject this DoSpecialDamage override into any projectile you like:

class DoomImpBall_Burn : DoomImpBall
{
	override int DoSpecialDamage (Actor victim, int damage, Name damagetype)
	{
		// Check the victim is valid, is a monster and is alive,
		// and check that this projectile has a valid target (shooter):
		if (victim && victim.bISMONSTER && victim.health > 0 && target)
		{
			// Give them the powerup and type-cast it to a pointer fc:
			let fc = ActorFireController(victim.GiveInventoryType('ActorFireController'));
			// If the cast was successfull, pass this projectile's
			// target to controller's fire_setter:
			if (fc)
			{
				fc.fire_setter = target;
			}
		}
		// Damage stays unmodified:
		return damage;
	}
}

This approach is vastly preferable to using actor replacements, because you have to implement it once, and then it'll just work on all actors. If you like, you can add more conditions to the controller or the DoSpecialDamage override, for example if (!victim.bBOSS) to make it not work on bosses, or if (!victim.bNOICEDEATH) to make it not work on monsters with the NOICEDEATH flag (like ArchVile or LostSoul).

Replacing existing actors

If you want your custom weapons, items, monsters, etc. to appear in Doom (or Heretic, Hexen, Strife, etc.) maps, you will need to have your actors replace the actors already in those maps. There are several ways to do that.

It is not possible to generically combine replacements from multiple mods at the same time. For example, if you load two mods that both replace the Doom's imp, you will only see the replacement from the latest mod in the autoload.

The replaces keyword

Both ZScript and DECORATE support the replaces keyword that can be utilized in the actor's definition:

class MyActor replaces ReplacementActor
{ ... }

You can also use inheritance:

class MyActor : ParentActor replaces ReplacementActor
{ ... }

In the example above, MyActor will inherit all properties, states, functions and such of ParentActor, and it will spawn in the map instead of ReplacementActor. Note, ParentActor and Replacement actor can be the same actor class. For example:

class DoomImp_Strong : DoomImp replaces DoomImp
{
  Default
  {
    Health 1000;
    +NOPAIN
  }
}

This creates a custom version of DoomImp that is identical to the default one, except it has 1000 health and will never enter its Pain state thanks to the NOPAIN flag. It will also spawn in the maps instead of the default DoomImp.

This method is easy to apply and is the most commonly used one (and before ZScript it was the only possible method). However, it has some downsides:

  1. The replaces method only offers 1:1 replacement: 1 original actor can be replaced with 1 new actor. You can't have 1 new actor replace multiple original ones, or vice versa.
  2. If multiple mods are loaded and each contains a replacement for the same default actor, the one loaded last will take precedence. This may be undesirable, for example, if you're creating a complex monster pack and a player launches it alongside a map that has its own monster replacements, because if the map is loaded last, its monsters will be loaded instead of the ones from the monster pack.

A common example of the latter is: imagine a map that contains replacements for monsters that serve only one purpose, to modify their BloodColor. To the author of the map this is a sensible stylistic choice, but it may cause issues when a player tries to load that map with a monster pack and accidentally put the mosnter pack earlier in the load order than the map.

CheckReplacement event

Event handlers offer a more robust method of replacement with the CheckReplacement() event. For example:

class MyReplacementHandler : EventHandler
{
	override void CheckReplacement(ReplaceEvent e)
	{
		if (e.Replacee == 'Zombieman')
		{
			e.Replacement = 'CustomZombieman';
		}
	}
}

This method is more flexible for a number of reasons. First, it takes priority over the replaces keyword, so you can force your replacements to take priority.

Second, it allows replacing multiple actors with the same one:

class MyReplacementHandler : EventHandler
{
	override void CheckReplacement(ReplaceEvent e)
	{
		let cls = e.Replacee;
		if (cls == 'Zombieman' || cls == 'ShotgunGuy' || cls == 'ChaingunGuy')
		{
			e.Replacement = 'WalkingCorpse';
		}
	}
}

This handler replaces Zombieman, ShotgunGuy and ChaingunGuy with the same new actor.

Third, you can implement any kind of custom logic you want into this system. For example, this will replace the Zombieman with a Cyberdemon with ~20% chance:

class MyReplacementHandler : EventHandler
{
	override void CheckReplacement(ReplaceEvent e)
	{
		if (e.Replacee == 'Zombieman' && random(0,10) >= 8)
		{
			e.Replacement = 'Cyberdemon';
		}
	}
}

More importantly, you can use CheckReplacement() to make replacements custom by letting the player change a custom CVAR and check for the value of that CVAR in CheckReplacement() before applying it.

Finally, the CheckReplacement() event has the e.IsFinal field. Some mods may set it to true to determine that their replacements should be final; other mods may check for it and only apply their replacements when it's false.

Note, to make sure actors replaced this way property handle their map death specials (like Arachnotrons and Mancubi on "Dead Simple" in Doom 2), you will need another event: CheckReplacee() This one is used to tell the game which actor exactly the new one replaces. For example:

class MyReplacementHandler : EventHandler
{
	override void CheckReplacement(ReplaceEvent e)
	{
		if (e.Replacee == 'Arachnotron')
		{
			e.Replacement = 'CustomArahnotron';
		}
	}
	override void CheckReplacee (ReplacedEvent e)
	{
		if (e.Replacement == 'CustomArahnotron')
		{
			e.Replacee = 'Arachnotron';
		}
	}
}

RandomSpawner

RandomSpawner is a class that can be utilized to have a random actor, from a specific list, spawn in place of another specific actor. RandomSpawner by itself doesn't offer any methods of replacement, it's just a way to define multiple replacements. After defining a custom RandomSpawner, you will still need to use one of the methods outlined above (either the replaces keyword, or the CheckReplacement() event) to have it spawn instead of one of the original actors.

Aside from randomized replacements, RandomSpawner automatically handles map death specials (like Arachnotrons and Mancubi on "Dead Simple" in Doom 2).

Special cases

Weapons

When a new weapon is created, simply having it replace one of the original weapons will not let the player properly use it. There are a few extra prerequisites:

  1. The replacement weapon needs to have Weapon.SlotNumber defined. If it doesn't, the player won't be able to select that weapon. (They can auto-switch to it when they receive or, or when they run out of ammo for other weapons, but they won't be able to select it by pressing a slot key.)
    Note, other methods of assigning slot numbers exist: this can be done in the PlayerPawn actor (your player class), the GameInfo block in MAPINFO, or even KEYCONF (deprecated). Using the SlotNumber property, however, is the easiest, most visible and most compatible method.
    Don't forget that if there are multiple weapons in the same slot (like the default Shotgun and SuperShotgun in Doom), you will also need to specify their order with Weapon.SlotPriority property.
  2. The replacement should also have Weapon.SelectionOrder defined. The lower this number, the higher the selection priority. When creating new weapons, make sure that their SelectionOrder is lower than that of the default weapons, otherwise the player may be auto-switched to one of the default weapons if they happen to have it in their inventory due to cheats, ACS scripts, etc.
  3. If the weapon is meant to be present in the player's inventory from the start, it will have to be defined as StartItem in your custom player class—see next section.

As such, if you've created, say, a Shotgun replacement, you will at the very least need this:

class Shotgun_new : Weapon replaces Shotgun
{
	Default
	{
		Weapon.SlotNumber 3;
		Weapon.SelectionOrder 130; //default Shotgun has 1300

		// the rest of the weapon code

Starting player items

Starting items that are given to the PlayerPawn when it spawns in the map are defined with the help of the Player.StartItem property. They're more complicated than the other cases, because they are not spawned in the map and they're not considered to be "given" (so, HandlePickup() cannot capture it). There are only two ways to replace starting items:

Define a custom player class and define your own start items. Remember that, if you create your class based on an existing one, like DoomPlayer, once you add even one startitem, the list defined in the original actor will be completely cleared. That's why if you want to add a custom pistol replacement, you will also need to add Fist and Clip again:

class MyPlayer : DoomPlayer
{
	Default
	{
		Player.StartItem "CustomPistol", 1; // a Pistol replacement
		Player.StartItem "Clip", 50;
		Player.StartItem "Fist", 1;
	}
}

This should normally be done via MAPINFO, but some mods may rely on the deprecated method of doing that via KEYCONF. Note, if you're aiming for high compatibility with your mod and want it to be playable with other mods, if those mods define their own player classes via KEYCONF, you will not be able to override them via MAPINFO, because KEYCONF takes precedence (that's the main reason why using it is not recommended).

The other method is to simply give starting items directly. There's no one correct way to do it, but one possible example is via an event handler:

class StartingItemsHandler : EventHandler
{
	override void PlayerSpawned(PlayerEvent e)
	{
		PlayerPawn pmo = players[e.PlayerNumber].mo;
		if (pmo)
		{
			pmo.ClearInventory(); //Remove all current items
			pmo.GiveInventory('Fist', 1);
			pmo.GiveInventory('Clip', 50);
			pmo.GiveInventory('CustomPistol', 1); //Pistol replacement
		}
	}
}

However, this specific example is not a good one because it will do this at the start of every map. Manual checks will need to be added to avoid this.

Keys

Replacing keys directly will not let your key replacements to open the same doors. In order to "link" a new key to the old key and let it open the same locks, you will need to use the Species property, as shown in this example.

If you're making your own map with custom keys, use the LOCKDEFS lump instead to set up keys and locks. However, in a gameplay mod without maps you absolutely should not modify LOCKDEFS, because it'll make your mod incompatible with map packs that have their own LOCKDEFS definitions.

Alternatively, if you want to add only minor cosmetic changes to your keys, you could modify their properties dynamically instead of replacing them.

In addition, it's also possible to use HandlePickup and a separate controller to simply make the player receive default keys (e.g. RedCard) whenever they're about to receive a custom key. See below.

Dynamic replacement of given items

If an item/weapon is given to the player directly (through an ACS script, a give console cheat, A_GiveInventory, or otherwise), it will not be subject to spawning/replacement rules. However, it may be captured with the help of a control item with a HandlePickup() override. You will find more information on the HandlePickup function page.

Another option is to use the CanReceive() virtual function inside your custom PlayerPawn (New from 4.12.2).

Replacement-related issues

Using any of the actor replacement methods outlined above will most of the time cause issues if combined with methods that explicitly check class names. This is mostly relevant only for gameplay mods that are meant to be combined with any custom map.

See also