Lua unit script: Polishing the stumpy

Introduction

The stumpy tutorial left the stumpy still rather unpolished. Only the core functionality was implemented.

In this tutorial we’ll see:

Killed

To customize what happens when the stumpy is killed you need to know about a single call-in: Killed. This call-in is called by Spring the moment the unit dies, and it receives as parameters the recent damage and the maximum health of the unit right before it died. It’s return value is the corpse that should be chosen from the chain created by following the corpse unit tag and any featuredead feature tags:

0                       1                            2
The Stumpy              ARMSTUMP_DEAD                ARMSTUMP_HEAP
corpse=ARMSTUMP_DEAD;   featuredead=ARMSTUMP_HEAP;   end

An example implementation could be:

function script.Killed(recentDamage, maxHealth)
	local severity = recentDamage / maxHealth
	if severity < 0.25 then
		Explode(barrel, SFX.NONE)
		Explode(base, SFX.NONE)
		Explode(turret, SFX.NONE)
		return 1   -- spawn ARMSTUMP_DEAD corpse
	elseif severity < 0.50 then
		Explode(barrel, SFX.FALL)
		Explode(base, SFX.NONE)
		Explode(turret, SFX.SHATTER)
		return 2   -- spawn ARMSTUMP_HEAP corpse
	else
		Explode(barrel, SFX.FIRE + SFX.SMOKE + SFX.EXPLODE)
		Explode(base, SFX.SHATTER)
		Explode(turret, SFX.SHATTER)
		return 3   -- unit completely disintegrates, no corpse
	end
end

What you see here is a more or less traditional implementation; the Killed script follows this pattern in many Lua (and BOS) scripts. Based on the severity of the death, defined as the ratio recentDamage over maxHealth, an animation is chosen that triggers some nice explosions, shatters a few pieces and/or makes them fly away.

Explode

The Explode call-out is used for this purpose; it takes a piece number and a set of flags (which are predefined in the SFX table) and triggers some predefined effects based on this.

The SFX constants interpreted by Explode are:

Note that Explode may be called outside Killed too. It really only spawns these effects and does not have any side effects on the model. That is, the pieces that are exploded do not suddenly turn invisible or anything: the piece argument is really only used to get the position of the effect.

Restore After Delay

This is not the name of a call-in or call-out, but the name of a common pattern (including a function of the same name) seen in many unit scripts. You may have noticed that our stumpy, after it has fought a few battles, keeps its barrel aimed in the direction it last fired: the barrel is not returned to its rest position. That is exactly what this pattern solves. It works like this:

local RESTORE_DELAY = 2 * 1000 * Spring.GetUnitWeaponState(unitID, 0, 'reloadTime')

local function RestoreAfterDelay()
	Sleep(RESTORE_DELAY)
	Turn(turret, y_axis, 0, 0.5)
	Turn(barrel, x_axis, 0, 0.25)
end

And AimWeapon1 has to be modified like this:

function script.AimWeapon1(heading, pitch)
	Signal(SIG_AIM)
	SetSignalMask(SIG_AIM)
	Turn(turret, y_axis, heading, 0.5)
	Turn(barrel, x_axis, -pitch, 0.25)
	WaitForTurn(turret, y_axis)
	WaitForTurn(barrel, x_axis)
	StartThread(RestoreAfterDelay)   --<<< insert this line
	return true
end

The clue of this pattern is that a (background) thread is started in AimWeapon which just sleeps for a while and then returns the turret and the barrel pieces to their initial direction. In this example the time to wait is set to twice the reload time of the weapon.

StartThread

The only new call-out seen here is StartThread. That may be a good reason to introduce you a little bit more to threads. The StartThread function creates a new processing thread in the unit script and runs it until the first Sleep, WaitForMove or WaitForTurn. Actually, thread is the wrong word (blame Lua for calling coroutines threads ;-)) as there is no parallel execution at all. You should see a thread in Lua unit scripts simply as a piece of code that may be suspended for a while when it executes one of Sleep, WaitForMove and WaitForTurn. While it is suspended other threads may run and the game may advance.

Smoke

Want units to emit a small plume of smoke when damaged? Here you go:

local random = math.random

function SmokeUnit(smokePieces)
	local n = #smokePieces
	while (GetUnitValue(COB.BUILD_PERCENT_LEFT) ~= 0) do
		Sleep(1000)
	end
	while true do
		local health = GetUnitValue(COB.HEALTH)
		if (health <= 66) then -- only smoke if less then 2/3rd health left
			EmitSfx(smokePieces[random(1,n)], SFX.BLACK_SMOKE)
		end
		Sleep(20*health + 200)
	end
end
function script.Create()
	Hide(flare)
	StartThread(SmokeUnit, {base, turret})  --<<< call it like this
end

This is a quite generic piece of code that makes your unit smoke. It runs in a background thread. At first, this thread waits until the unit has been fully built (otherwise smoke would be emitted when the unit is in the factory). Then it enters an infinite loop (it only ends when the unit gets destroyed) that emits smoke when the unit is damaged. The rate at which smoke is emitted increases with the amount of damage done to the unit. I’ll leave it as an exercise to the reader to figure out how exactly this works. The two new call-outs GetUnitValue and EmitSfx can be looked up on the wiki. (update: erm, I just noticed neither of them is explained properly there. I’ll try to cover them later :-))

Conclusion

In this tutorial I suggested some additions to the stumpy unit script created in the previous tutorial. You’ve (hopefully) learned something about the following call-in:

And the following call-outs:

Related posts