Overview
I have really been excited about the new scripting feature and have reworked most of my game to make use of it. For me I find it much easier to structure my game in code, but I still use the event sheets for some things.
As I have been working through my game I needed to use timers to have actions occur in multiple sub-scenes in my main layout. Of course I originally used the Timer plugin to manage this for me, but I wanted to have this managed in my JavaScript code. So this is what I came up with in my game and thought it might useful to others.
Original Implementation
I originally made use of the native JavaScript SetInterval and SetTimeout functions, which worked fine and I created a wrapper class to manage the timers. I used a manager class to keep track of multiple timers. Since I didn't end up going this route I am not actually going to list how I did it, but I ran into issues with how to manage pausing a game. So this idea was out.
Another Idea
So the next idea I had was to make use of the delta time from the construct 3 engine and track time passed and trigger functions based on elapsed time. This would involve the timers being checked every tick event. But this gives me the ability to pause a timer and have it pick right back up again. Great, this sounds like it'll work :)
So what I need is a manager class that is responsible for all timers and a timer info class that represents an actual timer.
But first - This issue
One of the first issues to deal with when using a class to call a method in another class directly without referencing the enclosing class is that it creates problems with the this context and prevents accessing other functions and variables in the class. What do I mean by this, well
Simple Timer Class
class Timer {
constructor(){
this.timers = new Array();
}
CreateTimer(funcToCall, params, timeDelta) {
this.timers.push({
funcToCall,
params, // Array of parameters
timeDelta
});
}
Tick() {
for (let timerObj of this.timers) {
timerObj.funcToCall(...params); //Spread operator to convert array to a
//list of parameters to pass into the function.
}
}
}
class MyGame
{
constructor() {
this.location='Test Facility';
this.timers = new Timer();
this.timers.CreateTimer(this.Handler, [], 3000);
}
Handler() {
console.log(this.location);
}
Tick() {
this.timers.Tick();
}
}
So in the code above I am passing a reference to the function Handler into the CreateTimer function to executed later. In that function I call a console log function to write out this.location. In this case I am expecting this to be a reference to the containing class. Under normal circumstances this works well, however since I am only passing a reference to a function to the create timer function, I lose the containing this class reference. What actually happens is that when the Timer Tick function calls the function, this is actually pointing to the Timer class. This means that this.location is undefined and the function call fails.
There are numerous ways to fix this issue, one way being passing another parameter to the CreateTimer function to store the class this, but then that has to be passed to the Handler function to use instead of this, which makes functions in the class look and behave differently from each other like:
class MyGame
{
constructor() {
this.location='Test Facility';
this.timers = new Timer();
this.timers.CreateTimer(this.Handler, [], 3000);
}
//Now this function behaves differently than the other functions of the class
Handler(context) {
console.log(context.location);
}
Tick() {
this.timers.Tick();
}
}
The way I decided to tackle this issue is to use arrow functions to enclose the function I am passing to the timer, which preserves the original context(this):
class MyGame
{
constructor() {
this.location='Test Facility';
this.timers = new Timer();
this.timers.CreateTimer(()=>{this.Handler();}, [], 3000);
}
Handler() {
console.log(this.location);
}
Tick() {
this.timers.Tick();
}
}
By using an arrow function, this is preserved and when the Handler is called this is pointing to the class and everything works. If you wanted to pass parameters in and call a Handler function that took two parameters you would do it like
this.timers.CreateTimer((item1, item2)=>{this.Handler(item1, item2);}, [item1, item2], 3000);
The parameters are being passed in as an array [item1, item2] so how does this work
timerObj.funcToCall(...params);
This is using the JavaScript spread operator, which spreads the array out into individual parameters to call a function like
So with this technique in mind, I implemented the following.
Implementation
Timer Info
This class needs to store all the information about whether the timer is repeating or only one time, what function to call, what parameters to pass, is it paused, and how many seconds before execution.
class TimerInfo
{
constructor(func, params, milliseconds, oneTime=true) {
this.oneTime=oneTime;
this.threshold = milliseconds;
this.current=0;
this.func = func;
this.params = params;
this.paused = false;
}
}
The code here is expecting the parameters to use to be passed in as an array.
This class is not actually used by a game directly, but is utilized by the TimerManager.
Timer Manager
This is the class that is used by a game to create timers, pause them, and clean them up.
Some assumptions for this class are:
- The construct 3 runtime reference is required
- Time is handled in milliseconds
- The JavaScript spread operator ... is used to take the array of parameters and pass them individually into a handler function
- The TimerManager Tick event needs to be called on each construct tick event.
class TimerManager
{
constructor(runtime) {
this.timers = new Map();
this.runtime = runtime;
}
Tick() {
this.timers.forEach((value, key, map)=>{
if (!value.paused) {
value.current += (this.runtime.dt * 1000.0);
if (value.current>=value.threshold) {
value.func(...value.params);
value.current = 0;
if (value.oneTime) {
map.delete(key);
}
}
}
});
}
Pause(tag, flag) {
const item = this.timers.get(tag);
if (item) {
item.paused = flag;
}
}
PauseAll(flag) {
this.timers.forEach((value, key, map)=>{
value.paused = flag;
});
}
ClearTimer(tag) {
if (this.timers.has(tag)) {
this.timers.delete(tag);
}
}
ClearAllTimers() {
this.CleanUp();
}
CleanUp() {
this.timers.clear();
}
CreateTimer(tag, func, params, milliseconds, oneTime=true) {
let item = this.timers.get(tag);
if (item) {
console.info(`Can not create a timer since ${tag} already exists.`);
}
else {
item = new TimerInfo(func, params, milliseconds, oneTime);
this.timers.set(tag, item);
}
}
}
Use Case
So how can we use this with arrow functions to preserve the proper this value.
First we need to have the start up code, where I will tie into the main tick event and define the timer manager.
StartUp.js
let gTimerManager, gGame;
runOnStartup(async runtime =>
{
runtime.addEventListener("beforeprojectstart", () =>
OnBeforeProjectStart(runtime));
});
function OnBeforeProjectStart(runtime)
{
gTimerManager= new TimerManager(runtime);
gGame = new Game(runtime);
runtime.addEventListener("tick", () => Tick(runtime));
}
function Tick(runtime)
{
gTimerManager.Tick();
gGame.Tick();
}
With this set up lets have a main game class that executes a repeating timer every 2.5 seconds.
class Game
{
constructor(runtime) {
this.runtime = runtime;
this.item1 = 'First';
this.item2 = 'Second';
gTimerManager.CreateTimer("Tag1",(i1)=>{this.HandleTimer(i1);}, [this.item1], 3000, false);
}
HandleTimer(i1) {
console.log(`${i1} - ${this.item2}`);
}
Tick() {
}
}
This will print out to the console First - Second every 3 seconds.
Wrap Up
Since the timer is actually just accumulating time, it easy to pause it and then resume. This can be implemented with native JavaScript timer functions, but it becomes much more complex to manage pausing. This is how I solved the issue in my game and maybe it can help someone out or give them ideas to try something else.