[JS] Extend the runtime object using a mixin

1

Features on these Courses

Stats

1,245 visits, 1,550 views

Translations

This tutorial hasn't been translated.

Tools

At the moment, the runtime object is pretty empty, and you might want to add you own code to it. Extending the runtime can be useful if you want to add your own utilitary functions and have them available everywhere runtime is also available. It's also better to have similar methods gathered inside a single object instead of having to create a new class, and instantiate it on the window scope.

Mixins

Mixins are pretty cool. The basic idea is to write methods inside an object, and then extend multiple classes using the methods that are inside said object. Here is a simple example of a mixin:

class Test{
  testfn(){
    console.log("Class testfn")
  }
}

let mixin = {
  testfn(){
    console.log("Mixin testfn")
  },  
  mixinfn(){
    console.log("Mixin function works")
  },
  testProperty: 15
}


Object.assign(Test.prototype, mixin);
let testInst = new Test()

testInst.mixinfn()
testInst.testfn()

This is the result of running that code in the console

> Mixin function works
> Mixin testfn

You'll notice that the mixin's function overwrote the Test class's method. That might be a desirable outcome in some cases, but not here. The runtime object is constantly evolving, and new functions might be added in the future, and you don't want your function to overwrite runtime functions and break everything overnight. Also, we're only extending the runtime object, not the whole class (especially since we don't have access to the runtime class)

Therefore, we'll need to make our own extend function.

function extend(obj1, obj2){
  Object.keys(obj2).forEach(key => {
    if(obj1.hasOwnProperty(key) || obj1.__proto__.hasOwnProperty(key)){
      console.warn("key " + key + " already exists")
      delete obj2[key]
    }
  })
  Object.assign(obj1, obj2);
}

This code will run through every property of the mixin and remove the ones that are already in runtime in order to never overwrite anything. It will also make sure to print a warning message in case something broke.

Now we can write this

class Test{
  testfn(){
    console.log("Class testfn")
  }
}

let mixin = {
  testfn(){
    console.log("Mixin testfn")
  },  
  mixinfn(){
    console.log("Mixin function works")
  },
  testProperty: 15
}

let testInst = new Test()
extend(testInst, mixin)

testInst.mixinfn()
testInst.testfn()

And now the result in the console is this

> Mixin function works
> Class testfn

Implementing this in C3

Create two script files. onStart.js that contains the default script code C3 fills in for the first script you add to your project and runtimeMixin.js that will contain the mixin.

This is my current runtimeMixin file:

let RuntimeMixin = {
	initMixin(){
		this.battleSystem = new BattleSystem(this, skills)
	},
	startCoroutine(coroutine){
	  	const coroutineId = id++
	  	//runningCoroutines.push(coroutine)
		return new Promise((resolve, reject) => {
		function progress(){
			let ret = coroutine.next()
		  	if(ret.done){
				resolve()
				delete tickCallbacks[coroutineId]
		  	} else {
				let promise
				let usePromise = true
				if(ret.value instanceof Promise){
					promise = ret.value
				} else if(ret.value instanceof CoroutineYield) {
					//console.log("Hey I am a coroutine Yield")
					promise = ret.value.process()
				}

				if(promise != undefined){
					delete tickCallbacks[coroutineId]
			  		promise.then(()=>{
						tickCallbacks[coroutineId] = progress
						progress()
			  		})
				}
		  	}
		}
		tickCallbacks[coroutineId] = progress
	  })
	}
	
}

I added initMixin that I use to initialise properties that I add with the mixins without having to write everything in onStart.js, and so when I extend runtime, I can call my initMixin method for more detailed initialisation.

(I also added a startCoroutine method but that's for another tutorial 👀)

runOnStartup(async runtime =>
{
	// Code to run on the loading screen
	
	runtime.addEventListener("beforeprojectstart", () => OnBeforeProjectStart(runtime));
	extend(runtime, RuntimeMixin);
	runtime.initMixin()
});

function extend(obj1, obj2){
	Object.keys(obj2).forEach(key => {
		if(obj1.hasOwnProperty(key) || obj1.__proto__.hasOwnProperty(key)){
			console.warn("key " + key + " already exists")
			delete obj2[key]
		}
	})
	Object.assign(obj1, obj2);
}

Note that while you can initialise properties directly in the mixin without an init method, you can only access runtime as this from within a method.

Let's go back to our initial test and show what I mean.

class Test{
  testfn(){
    console.log("Class testfn")
  }
}

let mixin = {
  init(){
    this.prop1 = this
  },  
  prop2: this
}

let testInst = new Test()

extend(testInst, mixin)

testInst.init()
console.log(testInst)

This is the result

prop1 has been assigned in the init method, and is a reference to the Test instance itself.

prop2 has been assigned in the mixin, and is a reference to the window object. This is because the this keyword is a reference to the instance when called from a method, because the method's scope is the class instance. However, that same this keyword is a reference to the scope around the mixin when called from outside of a method. In that case, it's the window.

Welp, this was a short tutorial, hope you can all make good use of this 😄

  • 0 Comments

  • Order by
Want to leave a comment? Login or Register an account!