Skip to content

Custom Tick Scheduling

UnlimitedHugs edited this page Mar 2, 2018 · 5 revisions

Sparkweed ticks

Visualized ticks of the Sparkweed plant from Remote Explosives (half speed)

Notice: this article describes tools found in the A18 (HugsLib 4.0) version of the library. While the tools can be found in previous versions, their older variants require various workarounds to be used safely.

The Problem

Ticking is an essential part of bringing your creations to life in Rimworld. The stock game offers 3 ticking intervals:

  • Normal: 60 times a second (roughly every frame)
  • Rare: every 250 Normal ticks (4.16 seconds)
  • Long: every 2000 Normal ticks (33.33 seconds)

The Normal interval is the workhorse for most modded and stock Things in the game. Depending on what you are making, one or two updates a second are often quite sufficient- the rest is wasted CPU time.

The Vanilla Solution

public override void Tick() {
	if ((Find.TickManager.TicksGame + thingIDNumber.HashOffset()) % 60 == 0) {
		// logic here
	}
}

Placed in a type that inherits from Thing, this allows code to be executed at one second intervals. The HashOffset part is optional- it prevents the game from stalling from many instances of the same Thing performing their work during the same tick.
While this is a workable approach in small doses, it quickly degrades the performance of the game if you need to tick hundreds of instances of your custom Thing.

The HugsLib Solution

HugsLib provides the DistributedTickScheduler to tackle this problem. It allows to efficiently tick a high number of recipients that use the same tick interval:

public class CustomThing : Thing {
	public override void SpawnSetup(Map map, bool respawningAfterLoad) {
		base.SpawnSetup(map, respawningAfterLoad);
		HugsLibController.Instance.DistributedTicker.RegisterTickability(CustomTick, 30, this);
	}

	private void CustomTick() {
		// logic here
	}
}

This registers a Thing to receive recurring ticks every half second (30 Normal ticks). Since vanilla ticks are not used, they should be disabled in the XML Def of your Thing:

<tickerType>Never</tickerType>

The calls will be uniformly distributed in time to maximize performance. This means that if we register 190 Things with a 30 tick interval (like in the above image), every game tick only 6 to 7 calls will be made.

The registered method will continue to be called until the Thing it is registered with is de-spawned, destroyed, or becomes discarded (which happens when a map is discarded). You can, however, unregister manually if you need to stop the ticks while your Thing is still spawned:

HugsLibController.Instance.DistributedTicker.UnregisterTickability(this);

TickDelayScheduler

HugsLib also includes a convenient way to create delays for a given number of ticks:

HugsLibController.Instance.TickDelayScheduler.ScheduleCallback(() => {
	// logic here
}, 10, this, false);

This registers a one-time callback to be executed in 10 game ticks from the time it is registered. Unlike with DistributedTickScheduler, providing a Thing to associate the callback with is optional. If provided, however, the callback will only fire if the Thing is still spawned at callback time.

The last argument allows to register a recurring callback that will be automatically rescheduled after it is called. Note, that DistributedTickScheduler is a more efficient solution if your plan is to tick a large number of Things with the same interval.

Callbacks can be easily unscheduled to cancel a delay or recurring call:

HugsLibController.Instance.TickDelayScheduler.TryUnscheduleCallback(MyMethod);

To have the ability to unregister a call, it is necessary to keep a reference to the delegate you registered, or to use a named method.

Finally, both DistributedTickScheduler and TickDelayScheduler will respect game pausing and time acceleration, and will only work if a game is in progress.

Related feature: DoLaterScheduler

Accessed via HugsLibController.Instance.DoLater, this scheduler allows to run one-time callbacks at a future event. This is useful if code needs to be run from the main thread, or to wait for the next frame or tick.
A new callback can be rescheduled the moment it is invoked. This allows to keep a recurring event listener until it is no longer needed. This example waits for 60 frames:

var numFrames = 0;
Action everyFrame = null;
everyFrame = () => {
	if (numFrames < 60) {
		HugsLibController.Instance.DoLater.DoNextUpdate(everyFrame);
		numFrames++;
	} else {
		// finally, do a thing
	}
};
everyFrame();

DoNextTick

Executes a callback at the start of the next game tick.

DoNextUpdate

Executes a callback at the start of the next frame (Unity Update).

DoNextOnGUI

Executes a callback at the start of the next OnGUI event.

DoNextMapLoaded

Executes a callback the next time a map has finished loading. Receives the loaded map instance as argument.