OOP Ideas for Screeps/JS
-
What are folks doing to integrate some OOP design into their Screeps code?
Background: I'm an experienced systems programmer but I'm brand new to Javascript, and it's not immediately clear to me how to integrate JS classes efficiently with the existing Screeps classes. I've been playing around in the console a bit and it seems like we can't just write
class MyCreep extends Creep { ... }
sadly, which is exactly what I want to do.I've been thinking about this in the back of my mind for a few days and I think a good construct would be
class MyCreep { constructor(creep_id) { ... } }
where all the properties are just setters/getters to/from the creep'smemory
, then wherever I deal withMyCreep
objects if I want to invoke a trueCreep
method (moveTo
, etc...) I'll just have to go through a special method/property likeMyCreep.creep
which would return the trueCreep
. Kind of like this:class MyCreep { creep; constructor(creep_id) { this.creep = Game.getObjectById(creep_id); } get memory() { return this.creep.memory; } get target() { return Game.getObjectById(this.memory.target_id); } set target(target_object) { this.memory.target_id = target_object.id; } myMoveTo(target_object) { this.target = target_object; // other state adjustments... }
Then I could use them like this:
var john = new MyCreep(Game.creeps['John Galt'].id); john.myMoveTo(john.creep.room.controller);
My biggest concern with this method is that
MyCreep
objects require the apparently superfluouscreep
member, and I have to actually allocate them withnew
- I would have to resort to_.forEach(Game.creeps, (creep) => {new MyCreep(creep.id)})
each tick. The systems side of me is triggered by the dynamic allocations incurred by the calls tonew MyCreep()
. At a high level there is no real extra state I need to capture that isn't already in theCreep
object itself to which I already have a reference. I find myself inclined to find a way to runMyCreep
methods directly on trueCreep
objects, wherethis
can refer to theCreep
itself. I'd want to use something like in the following pseudocode in-place ofnew
:new_MyCreep(creep_id) { var creep = Game.getObjectById(creep_id); return (MyCreep) creep; // indicates my desire for a "conversion" from Creep to MyCreep }
In my naiivete, I see this as avoiding the extra allocation of a dedicated
MyCreep
object, which now can just operate directly on the API-allocatedCreep
object. I would then like to use_.forEach(Game.creeps, new_MyCreep)
(instead ofnew MyCreep()
) and then use instances ofMyCreep
to invoke trueCreep
methods as well asMyCreep
methods, like:var john = new_MyCreep(Game.creeps['John Galt'].id); john.myMoveTo(john.room.controller);
I've seen various people mess with
.prototype
but I am unable refer toCreep.prototype
in the Screeps console andcreep_instance.prototype
isundefined
, so I've given up on pursuing that lead.I'm not well-versed enough in JS to know how dumb this all sounds, so please share your thoughts. Is my first method fine, and I'm just overestimating how expensive the extra
new
calls are? Is the second method even possible? What do you do?Cheers!
Fritz
-
Firstly, I would strongly recommend reading up on how Javascript's prototype system works, as this will probably clear up a lot of things for you. It's a bit confusing at first.
Very briefly:
.prototype
is only used to define properties on the constructor, which is actually a function:function Foo(name) { // this is the constructor // Example: assign some arbitrary properties for this instance this.name = name; } Foo.prototype.bar = function () { // this is a method }; // Create an instance let foo = new Foo('one'); foo.bar();
When you try to access
foo.bar
, it will first look up to see iffoo
has abar
of its own. If not, it will checkfoo
's prototype and see if that has abar
property. If not, it will check the next prototype up the chain and so on.Instances don't have
.prototype
. You can get the prototype of an instance using.__proto__
(unofficial, deprecated) orObject.getPrototypeOf(foo)
, but you should never normally need to do this.Since all objects in Javascript are dynamic, you can add methods to existing classes like this:
Creep.prototype.foo = function () { };
This method will then be available on all instances of Creep, e.g.
creep1.foo()
.Generally there are two approaches to structuring code in screeps:
- Extend the prototypes
- Wrap everything in your own classes
I would suggest starting with option 1, since even the players using option 2 end up doing this to some extent. This approach is much easier to understand, as you only have to think about the one Creep type, instead of juggling Creep and MyCreep everywhere.
There are pros and cons to wrapping everything in your own objects. As you've noted, it can be quite a performance hit to construct all these objects every tick. The way around this is to cache the MyCreep instances on the heap and just update the internal
.creep
references each tick. This is why I suggest leaving this approach until you are more familiar with the game. It's best to deal with game objects (like Creep, Room, etc) and Memory - leave the heap until later.Without wishing to confuse you, I'm currently using the prototype approach, but I hate the
.prototype
syntax so I use the nicer class syntax but then copy all the members onto the existing Creep class like this:class MyCreep { bar() { } } function extendClass(base, extra) { let descs = Object.getOwnPropertyDescriptors(extra.prototype); delete descs.constructor; Object.defineProperties(base.prototype, descs); } extendClass(Creep, MyCreep);
I never create a MyCreep instance, I just use the builtin Creep objects everywhere, but I can still access all the methods from MyCreep because they've been copied onto Creep.
I hope some of this is helpful!
-
@SystemParadox Yes, that's very helpful, thank you!!
Extending the prototypes makes sense - I guess I was thrown off because I tried entering
Creep.prototype
in the Screeps console, and I would seeError: This creep doesn't exist yet
. I guess trying to "print" the prototype somehow tries to instantiate a Creep, but playing with it again now I can see that I can absolutely domoveTo = Creep.prototype.moveTo
. I like yourextendClass
function, that makes a lot of sense (after reading the corresponding documentation). I agree that assigning directly to the prototype is ugly. This is pretty much exactly what I was hoping for!The way around this is to cache the MyCreep instances on the heap [...]
Do you mean serializing them to
Memory
, or is there another "heap" that offers some persistence? I was under the impression from reading the documentation that global state is reset every tick. Is this a change with the introduction of isolated VMs? Is it as simple as using a global object/array?var cached_creeps = {}; module.exports.loop = { if (!cached_creeps) { cached_creeps = [new MyCreep(Game.creeps.john_galt))]; } cached_creeps[0].say("I am " + cached_creeps[0].name); }
-
Extending the prototypes makes sense - I guess I was thrown off because I tried entering
Creep.prototype
in the Screeps console, and I would seeError: This creep doesn't exist yet
. I guess trying to "print" the prototype somehow tries to instantiate a CreepThis really threw me for a bit! You seem to have found the answer although you may not realise it so it's probably worth explaining exactly what's going on here!
Accessing
Creep.prototype
doesn't create anything. What's actually happening here is that the console calls.toString()
on the object (e.g.Creep.prototype.toString()
). When you callcreep1.toString()
it walks up the prototype chain and ends up callingCreep.prototype.toString()
, but thethis
context is stillcreep1
. If you callCreep.prototype.toString()
directly, thethis
context isCreep.prototype
. ThetoString()
function tries to getthis.name
, but it's a getter, and it throws an error if the object doesn't havethis.id
.This should work:
> !! Creep.prototype true
Do you mean serializing them to
Memory
, or is there another "heap" that offers some persistence? I was under the impression from reading the documentation that global state is reset every tick. Is this a change with the introduction of isolated VMs? Is it as simple as using a global object/array?var cached_creeps = {}; module.exports.loop = { if (!cached_creeps) { cached_creeps = [new MyCreep(Game.creeps.john_galt))]; } cached_creeps[0].say("I am " + cached_creeps[0].name); }
Yes that's exactly it. There are basically 3 kinds of memory in screeps:
- Game objects (e.g. Creep, Room, etc). These are only valid for one tick
- The heap - that is, global variables or module scoped variables as in your example. These will last until a "global reset", which happens somewhat randomly depending on server load, but is generally a few hundred ticks or so. A global reset will also occur when you update your code.
Memory
(includingcreep.memory
,room.memory
, etc). This is actually a bit of a misnomer. It's a flat JSON structure, so it's more like storing it on disk. Only cleared if you respawn.
I find it quite useful to use game objects to cache values that are only valid in the current tick. For example, I do things like this a lot:
Room.prototype.getRepairTargets = function () { if (! this._repairTargets) { this._repairTargets = this.find(FIND_MY_STRUCTURES, { filter: struct => struct.hits < struct.hitsMax, }) } return this._repairTargets; };
That way I can call
this.room.getRepairTargets()
on several different creeps but it will only callfind
once each tick.
-
I do not use it, but have seen or heard of multiple instances of game object wrappers that ppl have used (e.g. your
new MyCreep(creepID)
) and can provide some insight for ya.I've heard of ppl using
Proxy
objects to create a "persistent object" similar to what the devs said they intended to implement eventually (TM) about a year or so ago. Basically, internal to the proxy object is the game object's ID, and each new tick, it grabs that game object again, and proxies any function calls to the wrapped object. you could add additional functionality by adding new handlers to he proxy itself, and let the proxy route the actual intents to the game object.Also, there's the "initialize once, use multiple ticks" concept, which is how my Kernel, Scheduler and libraries work in my OS codebase. Basically, you initialize the object once, and assign it to
global
(global.kernel = new Kernel(Memory.SempOS)
) and it will persist as long as your global does. It will re-init every time you push your code, or get a global reset for some reason. With IVM, you only have a single global, and resets are considerably more rare, so it's feasible to do things like this whereas 2+ years ago, it would have been much more difficult. Tying this concept to game objects, you'd init the object with an object's ID, and it would cache the object for that tick. it'd looks something like this:function MyCreep(id) { this.id = id; this.creep = Game.getObjectById(this.id); this.tick = Game.time; } MyCreep.prototype.move = function(dir) { this.refreshCreep(); return this.creep.move(dir); } MyCreep.prototype.refreshCreep = function() { if(this.tick != Game.time) { this.creep = Game.getObjectById(this.id); this.tick = Game.time; } }
The third and final option is what you mentioned previously, which is initializing and disposing of new objects each creep so you dont need the
refreshCreep()
in the example above.Personally, I lean heavily on modifying the existing objects'
prototype
s, either throughObject.createProperty()
or just appending more functions onto it. That way, i dont get the potentially expensivenew
calls, and it just works.Hope this helps, @fritzy...
-
What's actually happening here is that the console calls
.toString()
on the object (e.g.Creep.prototype.toString()
). [...] The toString() function tries to getthis.name
, but it's a getter, and it throws an error if the object doesn't havethis.id
.Nice, yeah, I figured it would be part of the
toString
invocation. Good find!The heap - that is, global variables or module scoped variables as in your example. These will last until a "global reset" [...]
OK, makes sense. I suppose not all of the docs have been updated since the introduction of iVMs. Thanks, that's useful!
I do things like this a lot:
Room.prototype.getRepairTargets = function () { if (! this._repairTargets) { this._repairTargets = this.find(FIND_MY_STRUCTURES, { filter: struct => struct.hits < struct.hitsMax, }) } return this._repairTargets; };
That way I can call
this.room.getRepairTargets()
on several different creeps but it will only callfind
once each tick.Right. I found that JS supports the same "decorator" syntax as Python (a language with which I am much more familiar!) and I figured out how to write a decorator to seamlessly memoize the results of a property descriptor's getter, with the goal of simplifying this common pattern. I use it like:
class MyRoom { @memoize get repairTargets() { return this.find(FIND_MY_STRUCTURES, { filter: struct => struct.hits < struct.hitsMax }); } }
@SemperRabbit That tracks with what I'm learning - I might make a
Proxy
of my own. Thanks for your input!