Classes for common objects
Background
Two new optional properties were just added to droids: .cargoSize and .cargoUsed
On non-transporter droids, those properties will not be defined (as in the property itself will not exist).
In Javascript, when you try to access the value of a property that doesn't exist, you get an error. So you have to do some extra code for such properties to first check that the property is defined, and only then try to access its value.
I was going to suggest to Per that the properties always exist, but with a value of null
for non-transport objects.
But...
Droid objects are already large, closely followed by structure and feature objects. And the new template object is also pretty big.
When a script uses an enum*() function, large lists of these objects have to be composed by the C++ code and delivered to the JS environment. And, in many cases, the results of that work is used just once and then the objects are discarded. It's like capitalist consumerism applied to Warzone scripting – use once, then throw away. It's not good for the environment
Example properties
When we look at the Droid object, we can find a whole bunch of properties that can be considered "optional". That is to say, if they don't get added to the object, we can safely assume their value to be false
or null
. The list of such properties on droid objects is as follows:
- group
- armed
- isVTOL
- isRadarDetector
- isCB
- isSensor
- canHitAir
- canHitGround
- hasIndirect
- range
- cargoSlots
- cargoUsed
That's quite a lot of properties, eh?
For each of those properties, there is already an 'if' statement, and in most cases accompanied by an 'else' to set the default value.
Defaulting the values
We can provide defaults to all these values using prototypal inheritance – a standard feature of Javascript. It's very fast and easy to work with, and it would offer some other compelling benefits to scripters that I'll discuss later.
First, we need to define a class in the JS environment. It's important that this class is exposed to scripts, they will want to access it!
_global.WZDroid = function WZDroid() {}; // *must* be a function, not an object WZDroid.prototype.type = DROID; // no need to specify .type on droid instances again! // Then repeat for each of the other properties listed earlier: WZDroid.prototype.<property> = <defaultValue>; // where defaultValue is either true or null (as applicable)
We do this at the very start of the game when the scripting environment is created, before loading any .js files in to that environment.
Notes:
- It will be a massive help to scripters if the class function is named (ie. the function should have an immutable '.name' property) – this is so that the
typeof
operator works properly. - The class must be accessible to scripts running in the environment – this is so that the
instanceof
operator works properly.
So, we now have a function called WZDroid (the class) and we've defined defaults for the various "optional" droid properties on WZDroid.prototype. Now we need to make all the droid objects use this superclass.
In the JS API, there is a convDroid() function – this is where the inheritance of the WZDroid superclass will take place.
First, for each of the "optional" properties processed in convDroid(), remove the 'else' blocks – we no longer create the property if it will be using it's default value.
For example, the .range property gets changed so it looks like this:
if (range >= 0) { value.setProperty("range", range, QScriptValue::ReadOnly); } // else // { // value.setProperty("range", QScriptValue::NullValue); // }
Before returning the droid object, the following tweaks need making (and yes, this is my first ever attempt at C++ so excuse any bugs!):
- value.setProperty( "__proto__", engine->globalObject().property("WZDroid").property("prototype") );
- value.property("prototype").setProperty( "constructor", engine->globalObject().property("WZDroid") );
As far as Javascript is concerned, the droid object is now an instance of the WZDroid class.
This should reduce the number of properties on each droid instance, and significantly reduce the amount of data returned from enum*() functions.
But we can probably now make some additional optimisations, for example:
if (vtol droid) { set armed (only if droid has a weapon) set isVTOL if (transporter) { // I assume transports are a subclass of vtol? set cargoSlots set cargoUsed } } if (isWeapon) { set canHitAir (only if true) set canHitGround (only if true) set hasIndirect (only if true) set range } else { set isRadarDetector (only if true) set isCB property (only if true) set isSensor (only if true) set range? // would be great if there were separate .sensorRange / .weaponRange properties ;) }
So, if the droid isn't a weapon, no need to even check what it's weapon is capable of because we don't need to bother about creating those falsey properties any more (they will default to false or null due to droid inheriting WZDroid over on the JS side of things).
Rinse, wash, repeat
We can apply the same technique to the following objects:
- Structure object – WZStructure class defaults:
- .type = STRUCTURE
- .status = BUILT
- .stattype = DEFENSE
- .modules = null
- .group = null
- .isSensor = false
- .isCB = false
- .isRadarDetector = false
- .isECM = false (not sure if this prop even exists yet?)
- .canHitAir = flase
- .canHitGround = false
- .hasIndirect = false
- Feature object – WZFeature class defaults:
- .type = FEATURE
- .player = 99 (only oil drums, artifacts and oil resources will need to specify a player at instance level)
- .damageable = true
- .group = null
- .stattype = null?
- Template object – I've only just noticed this was added, but it too should be able to set some defaults:
- .type = TEMPLATE_DATA? (looks like there is no .type for this object yet!)
- .brain = null?
- .repair = null?
- .ecm = null?
- .sensor = null?
- .construct = null?
Going even further - base object
Droids, Structures and Features all inherit from the 'base obect', or what I tend to refer to as a Game object, so it makes sense to have the WZDroid, WZStructure and WZFeature classes all have a WZGameObject superclass.
First, we need to define WZGameObject within the scripting environment:
_global.WZGameObject = function() {}; WZGameObject.prototype.selected = false;
While this might seem a lot of extra work to default just one property, there are huge benefits to us scripters which I'll explain later.
Then we can apply the superclass to our classes...
// remember, this only happens once, when scripting engine initialised _global.WZGameObject = function() {}; WZGameObject.prototype.selected = false; _global.wzProto = new WZGameObject; _global.WZDroid = function WZDroid() {}; WZDroid.prototype.__proto__ = wzProto; WZDroid.prototype.type = DROID; WZDroid.prototype.<property> = <defaultValue>; // for each default property _global.WZStructure = function WZStructure() {}; WZStructure.prototype.__proto__ = wzProto; WZStructure.prototype.type = STRUCTURE; WZStructure.prototype.<property> = <defaultValue>; // for each default property _global.WZFeature = function WZFeature() {}; WZFeature.prototype.__proto__ = wzProto; WZFeature.prototype.type = FEATURE; WZFeature.prototype.<property> = <defaultValue>; // for each default property
From the scirpters' perspective...
Note: These are just super-basic examples for illustration purposes. Things get much more exciting with accessor properties (getter/setters). Anyway...
// typeof and instanceof operators suddenly become useful! someDroid instanceof WZGameObject // true (fast way to check if object is a game object) someDroid instanceof WZDroid // true (faster than someDroid.type == DROID) typeof someDroid // "WZDroid" // we can now create our own methods for game objects!! WZDroid.attackObj = function(obj) { if (this.droidType != DROID_WEAPON) return; // only weapons can attack // ...could also check health level, not going for repairs, etc. orderDroidObj(this, DORDER_ATTACK, obj); } someDroidObj.attack(someEnemyDroid); // :) // which in turn let's us do stuff like this... var goAttack = function attackObjWith(myDroid) { myDroid.attackObj(this); } function eventAttacked(victim, enemyTarget) { enumRange(victim.x, victim.y, 10, ALLIES).forEach( goAttack, enemyTarget ); } // or imagine stuff like this... WZDroid.huntForOil = function() { .... }; WZDroid.scoutArea = function(areaObj) { .... }; // starting to make more sense now? :) // we can add our own methods to all three types of object: wzProto.transferTo = function(player) { if (this instanceof WZFeature) return; // see, using it already! donateObject(this, player); // assuming this will soon handle structure objects as well } someFeature.transferTo(2); // does nothing someStructure.transferTo(2); // transfers to player 2 someDroid.transferTo(2); // transfers to player 2
Thinking to the future...
The game objects are likely to grow some more as time goes on. Most of the time, scripts are only looking at a set of specific properties per enum*(), a lot of data is being put in to those objects and then not getting used.
Once we have the objects inheriting classes, we could actually define getter/setter properties on the class.
For example:
// this is how to define a getter/setter in JS, no idea how it's done in C++ Object.defineProperty( WZDroid.prototype, "isVTOL", { get: function() { try { return isVTOL(this); } catch(e) { /* ignore errors */ } }, enumerable: true, configurable: false } );
This means that the C++ convDroid() function no longer creates a .isVTOL property. If the calling script access the <someDroid>.isVTOL property, it runs the get() function and returns its value.
Now, I'm not suggesting we do this any time soon. Much thought is required before taking this extra step. For example, if in eventDestroyed(victim) the script did "if (victim.isVTOL) ..." the JS APIs isVTOL function will fail because the droid is no longer on the map (I learnt this the hard way while working on the Enhanced SitRep mod).
But there might be some properties that could be pushed on to the WZDroid class in this manner, for example .action (never used in eventDestroyed, and rarely used anywhere else – although nutters like me do use it so don't remove it please!!). Another one is .armed – you don't care if a destroyed droid was armed or not heh. A final one is .range – again, nobody cares about this on a dead droid.
With just those three properties moved from instances to the WZDroid prototype, as a getter/setter properties (but with no setter, they're read-only), that could amount to tens of thousands of fewer property definitions in the space of a game (eg. 1500 droids on map, enumDroid() being called on a regular basis over a 10+ minute period...).
Anyway, I'll leave it there. The main desire is to get default static values put on to classes, this last section is just a ponder of where things could go in the distant future.