Classes:RandomSpawner

From ZDoom Wiki
(Redirected from RandomSpawner)
Jump to navigation Jump to search
Note: Wait! Stop! Before you copy this actor's definition into your mod, remember the following things:
  1. You do NOT need to copy that actor, since it is already defined.
  2. In fact, it's not just useless, it will cause problems.
  3. If you want to modify it, or use a modified version, using inheritance is the way to go.
  4. The actor definitions here are put on the wiki for reference purpose only. Learn from them, don't copy them.
Random spawner
Actor type Internal Game MiniZDoomLogoIcon.png (ZDoom)
DoomEd Number None Class Name RandomSpawner


Classes: RandomSpawner


The RandomSpawner is a special actor which randomly spawns one of a series of actors specified with the DropItem property in its DECORATE code.

The RandomSpawner should not be used directly. Instead, authors should derive a new class from the actor and specify the actors they wish to be randomly spawned in the code of the new actor.

In addition to specifying the actors that may spawn, two optional parameters may be specified for each entry in the list. The first is an integer used to specify the spawn chance of any given monster in the list, assuming it is selected to spawn, with 0 being never and 255 being always (default is 255). The second number specifies this actor's "weight" or chance of spawning versus other actors in the list (default is 1).

It is possible for RandomSpawners to spawn other RandomSpawners. However, to prevent infinite recursion loops, an error marker will instead be created if more than 32 random spawners get nested in such a way.

Examples

Basic example

This will create a random spawner which when placed in the map will always spawn one of four enemies with equal chance:

ZScript:

class MySpawner : RandomSpawner
{
    Default
    {
        DropItem "ZombieMan";
        DropItem "DoomImp";
        DropItem "HellKnight";
        DropItem "BaronOfHell";
    }
}

DECORATE:

actor MySpawner : RandomSpawner 1111
{
    DropItem "ZombieMan"
    DropItem "DoomImp"
    DropItem "HellKnight"
    DropItem "BaronOfHell"
}

Note: when using ZScript, editor numbers (aka DoomEdNums) have to be defined via MAPINFO.

Use of parameters

This code will create a spawner which will randomly choose from among four enemies with varying chance, and will sometimes spawn nothing when one of the larger enemies is chosen:

ZScript:

class MySpawner : RandomSpawner
{
    Default
    {
        DropItem "ZombieMan", 255, 10;
        DropItem "DoomImp", 255, 8;
        DropItem "HellKnight", 128, 4;
        DropItem "BaronOfHell", 64, 1;
    }
}

DECORATE:

actor MySpawner : RandomSpawner 1112
{
    DropItem "ZombieMan", 255, 10
    DropItem "DoomImp", 255, 8
    DropItem "HellKnight", 128, 4
    DropItem "BaronOfHell", 64, 1
}

Both parameters affect the likelihood that one of the actors will appear, but in different ways.

The first affects the probability that it will appear if selected. Using 255 is the equivalent of omitting the parameter entirely, and guarantees that the actor will spawn when selected. In this case, the Zombieman and Imp will always spawn if selected. The Hell Knight, if selected, will only spawn half the time, and the Baron will only spawn 1/4th of the time. Otherwise nothing will spawn at all.

The second weighs the probability the actor will be selected. Here, the Zombieman will be selected 10 times on 23 (about 43% of the times); ten is its weight and 23 is the total weight of all actors (10+8+4+1).

Keyword-based replacer

A random spawner can be used to replace a given actor with a randomly-chosen one. There is one caveat, though: the replaced actor cannot be spawned directly. For example, the following code will create a spawner which replaces all the zombies in the map and will spawn either a pistol zombie (75% chance) or a shotgun zombie (25% chance). Note the creation of the ZombieMan2 actor to avoid creating a recursion with the spawner spawning a copy of itself if it selects a ZombieMan.

ZScript:

class ZombieMan2 : ZombieMan {}

class ZombieReplacer : RandomSpawner replaces ZombieMan
{
    Default
    {
        DropItem "ZombieMan2", 255, 3;
        DropItem "ShotgunGuy", 255, 1;
    }
}

DECORATE:

actor ZombieMan2 : ZombieMan {}

actor ZombieReplacer : RandomSpawner replaces ZombieMan
{
    DropItem "ZombieMan2", 255, 3
    DropItem "ShotgunGuy", 255, 1
}

Editor number-based replacer

To avoid the inconvenience of having to define clones of replaced actors, it is possible to instead give the random spawner the doom editor number of the actor to replace. Our previous example would then be:

ZScript:

In ZScript, editor numbers have to be given via MAPINFO:

// in MAPINFO:
DoomEdNums
{
   3004 = ZombieReplacer
}

DECORATE:

In DECORATE editor numbers can be given directly:

actor ZombieReplacer : RandomSpawner 3004
{
    DropItem "ZombieMan", 255, 3
    DropItem "ShotgunGuy", 255, 1
}

3004 is the doom editor number of the ZombieMan, so he is replaced when the map is initialized. The advantage of this method is that this makes the spawner more compatible with other mods (for example, one that attaches dynamic lights to its firing frame, or one that replaces them with a modified actor to achieve certain special effects). The drawback is that only actors directly placed on the map in the editor will be replaced; not those spawned by ACS or other custom actors.

Important note: for replacing boss monsters (Arachnotron, BaronOfHell, Cyberdemon, Fatso, Ironlich, Minotaur, Sorcerer2 and SpiderMastermind), you need to use the replaces keyword or the game will not be able to know that the boss monsters are being replaced. In ZScript you can also use the CheckReplacement() event.

A spawner normally removes itself entirely from the game after spawning a monster; but if it finds the BOSS or BOSSDEATH flag on a monster it replaces or spawns, it will stay around and monitor its health so as to call A_BossDeath when the monster dies. It is the spawner, not the monster itself, which will trigger the map's special action in such a scenario.

For a scripted map, as long as the monsters which must trigger special actions have the special directly set on them or are identified by a TID rather than by their type, the random spawners should work correctly. However, scripts which identify monsters by their type (using functions such as ThingCount or ThingCountName) will be broken unless the spawned replacements are made as bosses.

Base RandomSpawner definition

ZScript definition

Note: The ZScript definition below is for reference and may be different in the current version of GZDoom.The most up-to-date version of this code can be found on GZDoom GitHub.
class RandomSpawner : Actor
{
	
	const MAX_RANDOMSPAWNERS_RECURSION = 32; // Should be largely more than enough, honestly.
	
	Default
	{
		+NOBLOCKMAP
		+NOSECTOR
		+NOGRAVITY
		+THRUACTORS
	}
	
	virtual void PostSpawn(Actor spawned)
	{}
	
	static bool IsMonster(DropItem di)
	{
		class<Actor> pclass = di.Name;
		if (null == pclass)
		{
			return false;
		}

		return GetDefaultByType(pclass).bIsMonster;
	}

	// Override this to decide what to spawn in some other way.
	// Return the class name, or 'None' to spawn nothing, or 'Unknown' to spawn an error marker.
	virtual Name ChooseSpawn()
	{
		DropItem di;   // di will be our drop item list iterator
		DropItem drop; // while drop stays as the reference point.
		int n = 0;
		bool nomonsters = sv_nomonsters || level.nomonsters;

		drop = di = GetDropItems();
		if (di != null)
		{
			while (di != null)
			{
				bool shouldSkip = (di.Name == 'None') || (nomonsters && IsMonster(di));
				
				if (!shouldSkip)
				{
					int amt = di.Amount;
					if (amt < 0) amt = 1; // default value is -1, we need a positive value.
					n += amt; // this is how we can weight the list.
				}

				di = di.Next;
			}
			if (n == 0)
			{ // Nothing left to spawn. They must have all been monsters, and monsters are disabled.
				return 'None';
			}
			// Then we reset the iterator to the start position...
			di = drop;
			// Take a random number...
			n = random[randomspawn](0, n-1);
			// And iterate in the array up to the random number chosen.
			while (n > -1 && di != null)
			{
				if (di.Name != 'None' &&
					(!nomonsters || !IsMonster(di)))
				{
					int amt = di.Amount;
					if (amt < 0) amt = 1;
					n -= amt;
					if ((di.Next != null) && (n > -1))
						di = di.Next;
					else
						n = -1;
				}
				else
				{
					di = di.Next;
				}
			}
			if (di == null)
			{
				return 'Unknown';
			}
			else if (random[randomspawn]() <= di.Probability)	// prob 255 = always spawn, prob 0 = almost never spawn.
			{
				return di.Name;
			}
			else
			{
				return 'None';
			}
		}
		else
		{
			return 'None';
		}
	}
	
	// To handle "RandomSpawning" missiles, the code has to be split in two parts.
	// If the following code is not done in BeginPlay, missiles will use the
	// random spawner's velocity (0...) instead of their own.
	override void BeginPlay()
	{
		Super.BeginPlay();
		let s = ChooseSpawn();

		if (s == 'Unknown') // Spawn error markers immediately.
		{
			Spawn(s, Pos, NO_REPLACE);
			Destroy();
		}
		else if (s == 'None') // ChooseSpawn chose to spawn nothing.
		{
			Destroy();
		}
		else
		{
			// So now we can spawn the dropped item.
			// Handle replacement here so as to get the proper speed and flags for missiles
			Class<Actor> cls = s;
			if (cls != null)
			{
				Class<Actor> rep = GetReplacement(cls);
				if (rep != null)
				{
					cls = rep;
				}
			}
			if (cls != null)
			{
				Species = Name(cls);
				readonly<Actor> defmobj = GetDefaultByType(cls);
				Speed = defmobj.Speed;
				bMissile |= defmobj.bMissile;
				bSeekerMissile |= defmobj.bSeekerMissile;
				bSpectral |= defmobj.bSpectral;
			}
			else
			{
				A_Log(TEXTCOLOR_RED .. "Unknown item class ".. s .." to drop from a random spawner\n");
				Species = 'None';
			}
		}
	}

	// The second half of random spawning. Now that the spawner is initialized, the
	// real actor can be created. If the following code were in BeginPlay instead,
	// missiles would not have yet obtained certain information that is absolutely
	// necessary to them -- such as their source and destination.
	override void PostBeginPlay()
	{
		Super.PostBeginPlay();

		if (bouncecount >= MAX_RANDOMSPAWNERS_RECURSION)	// Prevents infinite recursions
		{
			Spawn("Unknown", Pos, NO_REPLACE);		// Show that there's a problem.
			Destroy();
			return;
		}

		Actor newmobj = null;
		bool boss = false;

		if (Species == 'None')
		{
			Destroy();
			return;
		}
		Class<Actor> cls = Species;
		if (bMissile && target && target.target) // Attempting to spawn a missile.
		{
			if ((tracer == null) && bSeekerMissile)
			{
				tracer = target.target;
			}
			newmobj = target.SpawnMissileXYZ(Pos, target.target, cls, false);
		}
		else 
		{		
			newmobj = Spawn(cls, Pos, NO_REPLACE);
		}
		if (newmobj != null)
		{
			// copy everything relevant
			newmobj.SpawnAngle = SpawnAngle;
			newmobj.Angle		= Angle;
			newmobj.Pitch		= Pitch;
			newmobj.Roll		= Roll;
			newmobj.SpawnPoint = SpawnPoint;
			newmobj.special    = special;
			newmobj.args[0]    = args[0];
			newmobj.args[1]    = args[1];
			newmobj.args[2]    = args[2];
			newmobj.args[3]    = args[3];
			newmobj.args[4]    = args[4];
			newmobj.special1   = special1;
			newmobj.special2   = special2;
			newmobj.SpawnFlags = SpawnFlags & ~MTF_SECRET;	// MTF_SECRET needs special treatment to avoid incrementing the secret counter twice. It had already been processed for the spawner itself.
			newmobj.HandleSpawnFlags();
			newmobj.SpawnFlags = SpawnFlags;
			newmobj.bCountSecret = SpawnFlags & MTF_SECRET;	// "Transfer" count secret flag to spawned actor
			newmobj.ChangeTid(tid);
			newmobj.Vel	= Vel;
			newmobj.master = master;	// For things such as DamageMaster/DamageChildren, transfer mastery.
			newmobj.target = target;
			newmobj.tracer = tracer;
			newmobj.CopyFriendliness(self, false);
			// This handles things such as projectiles with the MF4_SPECTRAL flag that have
			// a health set to -2 after spawning, for internal reasons.
			if (health != SpawnHealth()) newmobj.health = health;
			if (!bDropped) newmobj.bDropped = false;
			// Handle special altitude flags
			if (newmobj.bSpawnCeiling)
			{
				newmobj.SetZ(newmobj.ceilingz - newmobj.Height - SpawnPoint.Z);
			}
			else if (newmobj.bSpawnFloat) 
			{
				double space = newmobj.ceilingz - newmobj.Height - newmobj.floorz;
				if (space > 48)
				{
					space -= 40;
					newmobj.SetZ((space * random[randomspawn]()) / 256. + newmobj.floorz + 40);
				}
				newmobj.AddZ(SpawnPoint.Z);
			}
			if (newmobj.bMissile && !(newmobj is 'RandomSpawner'))
				newmobj.CheckMissileSpawn(0);
			// Bouncecount is used to count how many recursions we're in.
			if (newmobj is 'RandomSpawner')
				newmobj.bouncecount = ++bouncecount;
			// If the spawned actor has either of those flags, it's a boss.
			if (newmobj.bBossDeath || newmobj.bBoss)
				boss = true;
			// If a replaced actor has either of those same flags, it's also a boss.
			readonly<Actor> rep = GetDefaultByType(GetReplacee(GetClass()));
			if (rep && (rep.bBossDeath || rep.bBoss))
				boss = true;

			PostSpawn(newmobj);
		}
		if (boss)
			tracer = newmobj;
		else	// "else" because a boss-replacing spawner must wait until it can call A_BossDeath.
			Destroy();
	}

	override void Tick()	// This function is needed for handling boss replacers
	{
		Super.Tick();
		if (tracer == null || tracer.health <= 0)
		{
			A_BossDeath();
			Destroy();
		}
	}

}

DECORATE definition

Note: This is legacy code, kept for archival purposes only. DECORATE is deprecated in GZDoom and is completely superseded by ZScript. GZDoom internally uses the ZScript definition above.
ACTOR RandomSpawner native
{
  +NOBLOCKMAP
  +NOSECTOR
  +NOGRAVITY
  +THRUACTORS
}

See also