Monday, April 6, 2015

software engineering - Entity Component System based engine


Note: I'm programming this in Javascript, but it should be language agnostic in the most part.



I am thinking about converting my engine to an ECS based one.


I get the basic idea (note: this is wrong, see my answer):


Entities are game objects.
Components are bits of functionality (reactToInput()) or state (position) which can get "glued" to entities.
Systems have a list of entities they manage and update.


But, I'm not quite sure that I get the implementation and some details...


Question: can a system operate on different kinds of entities? I usually give the example of a class called Scene in my engine, and it will serve this purpose now, as well. A scene is a container of all objects that can be rendered, updated, affect the rendering (lights), and maybe, in future, even 2DSoundEmitter objects. It has a high-level interface so the user doesn't need to worry about the type of the object he's scene.add()ing, and all that kind of stuff.


I realize that the Scene could be a system. It takes in entities, stores them, and then it can call their update methods, and maybe even do some state changes. But, there is a problem: as I described above, the Scene can be fed different types of objects! What should I do in, say, a situation where a scene has both renderable objects ("drawables") and lights in it? Should I make it type-check entities before interacting? Or, should I solve it at an even lower level: make a LightSource component which can be added to any object, and the light would just be an entity with LightSource and Position components. Is that acceptable?


Also, is it a good practice to still use conventional inheritance and traditional classes? For example, I just can't figure out what would my Renderer be! It's not a system, as its only function is to take in a camera and a scene, render everything and apply effects (such as shadows). It also manages the context, the width and the height of the game, makes translations... But it's still not a system!


Edit: could you maybe link any resources you found on the ECS? I'm having trouble finding good ones.




Answer



Let me see if by trying to understand as a web/UI JS dev, I can be of help. Also, don't go too far in language agnosticism. A lot of patterns established in other languages are worth studying but can be applied very differently in JS due to its flexibility or really aren't necessary due to the malleable nature of the language. You could blow some opportunities if you write your code thinking of JS as having the same set of boundaries that a more classical OOP-oriented language does.


First of all, on the "don't use OOP" factor, remember that JavaScript objects are like playdough compared to other languages and you actually have to go out of your way to build a cascading-inheritance scheme nightmare since JS isn't class-based and compositing comes much more naturally to it. If you are implementing some silly class or prototype hand-me-down system in your JS, consider ditching it. In JS we use closures, prototypes, and we pass functions around like candy. It's disgusting and filthy and wrong but also powerful, concise and that's the way we like it.


Inheritance heavy approaches are actually spelled out as an anti-pattern in Design Patterns and for good reason as anyone who has walked down 15+ levels worth of class or class-like structures to try and figure out where the heck the busted version of a method was coming in from can tell you.


I don't know why so many programmers love doing this (especially java guys writing JavaScript for some reason), but it's awful, illegible, and completely unmaintainable when used to excess. Inheritance is okay here and there, but not really necessary in JS. In languages where it's a more enticing shortcut, it should really be reserved for more abstract architecture concerns rather than more literal modeling schemes like frankensteining a zombie implementation through an inheritance chain that included a BunnyRabbit because it happened to work. That's not good code reuse. It's a maintenance nightmare.


As a JS dev Entity/Component/System based engines strike me as a system/pattern for decoupling design concerns and then compositing objects for implementation on a highly granular level. In other words, child's play in a language like JavaScript. But let me see if I'm grokking this correctly first.




  • Entity - The specific thing you are designing. We're talking more in the direction of proper nouns (but not actually, of course). Not 'Scene', but 'IntroAreaLevelOne'. IntroAreaLevelOne might sit inside a sceneEntity box of some kind but we're focusing on something specific that varies from other related things. In the code, an entity is really just a name (or ID) tied to a bunch of stuff that it needs to have implemented or established (the components) in order to be useful.





  • Components - types of things an entity needs. These are general nouns. Like WalkingAnimation. Within WalkingAnimation we can get more specific, like "Shambling" (good choice for zombies and plant monsters), or "ChickenWalker" (great for reverse-joint ed-209ish robot-types). Note: Not sure how that could decouple from the rendering of a 3D model like that - so maybe a crap example but I'm more of a JS pro than an experienced game dev. In JS I would put the mapping mechanism in the same box with the components. Components in their own right are likely to be light on logic and more of a roadmap telling your systems what to implement if systems are even needed (in my attempt at ECS some components are just collections of property sets). Once a component is established, it's easy enough to tweak one component and assign the altered version to a new name if necessary but having sets of properties that can referred to for branching of behavior established by Systems strikes me as a better way to go.




  • Systems - The real programmey meat is here. AI systems are built and linked, Rendering is achieved, animations sequences established, etc... I'm copping out and leaving these mostly to the imagination but in the example System.AI takes a bunch of properties in and spits out a function which is used to add event handlers to the object that ultimately gets used in implementation. The key thing about System.AI is that it covers multiple component types. You could sort out all the AI stuff with one component but to do so is to misunderstand the point of making things granular.




Mind the Goals: We want to make it easy to plug in some kind of GUI interface for non-designers to easily tweak different kinds of stuff by maxing and matching components within a paradigm that makes sense to them, and we want to get away from popular arbitrary code schemes that are a lot easier to write than they are to modify or maintain.


So in JS, maybe something like this. Game devs please tell me if I've got it horribly wrong:


//I'm going with simple objects of flags over arrays of component names

//easier to read and can provide an opt-out default
//Assume a genre-bending stealth assassin game

//new (function etc... is a lazy way to define a constructor and auto-instantiate
var npcEntities = new (function NpcEntities(){

//note: {} in JS is an object literal, a simple obj namespace (a dictionary)
//plain ol' internal var in JS is akin to a private member
var default={ //most NPCs are humanoids and critters - why repeat things?
speedAttributes:true,

maneuverAttributes:true,
combatAttributes:true,
walkingAnimation:true,
runningAnimation:true,
combatAnimation:true,
aiOblivious:true,
aiAggro:true,
aiWary:true, //"I heard something!"
aiFearful:true
};


//this. exposes as public

this.zombie={ //zombies are slow, but keep on coming so don't need these
runningAnimation:false,
aiFearful:false
};

this.laserTurret={ //most defaults are pointless so ignore 'em
ignoreDefault:true,

combatAttributes:true,
maneuverAttrubtes:true, //turning speed only
};
//also this.nerd, this.lawyer and on and on...

//loop runs on instantiation which we're forcing on the spot

//note: it would be silly to repeat this loop in other entity collections
//but I'm spelling it out to keep things straight-forward.
//Probably a good example of a place where one-level inheritance from

//a more general entity class might make sense with hurting the pattern.
//In JS, of course, that would be completely unnecessary. I'd just build a
//constructor factory with a looping function new objects could access via
//closure.

for(var x in npcEntities){

var thisEntity = npcEntities[x];

if(!thisEntity.ignoreDefaults){


thisEntity = someObjectXCopyFunction(defaults,thisEntity);
//copies entity properties over defaults

}
else {
//remove nonComponent property since we loop again later
delete thisEntity.ignoreDefaults;
}
}

})() //end of entity instantiation

var npcComponents = {
//all components should have public entityMap properties

//No systems in use here. Just bundles of related attributes
speedAttributes: new (function SpeedAttributes(){
var shamblingBiped = {
walkingAcceleration:1,
topWalking:3

},
averageMan = {
walkingAcceleration:3,
runningAcceleration:4,
topWalking: 4,
topRunning: 6
},
programmer = {
walkingAcceleration:1,
runningAcceleration:100,

topWalking:2
topRunning:2000
}; //end local/private vars

//left is entity names | right is the component subcategory
this.entityMap={
zombie:shamblingBiped,
lawyer:averageMan,
nerd:programmer,
gCostanza:programmer //makes a cameo during the fire-in-nursery stage

}
})(), //end speedAttributes

//Now an example of an AI component - maps to function used to set eventHandlers
//functions which, because JS is awesome we can pass around like candy
//I'll just use some imaginary systems on this one

aiFearful: new (function AiFearful(){
var averageMan = Systems.AI({ //builds and returns eventSetting function
fearThreshold:70, //%hitpoints remaining

fleeFrom:'lastAttacker',
tactic:'avoidIntercept',
hazardAwareness:'distracted'
}),
programmer = Systems.AI({
fearThreshold:95,
fleeFrom:'anythingMoving',
tactic:'beeline',
hazardAwareness:'pantsCrappingPanic'
});//end local vars/private members



this.entityMap={
lawyer:averageMan,
nerd:averageMan, //nerds can run like programmers but are less cowardly
gCostanza:programmer //makes a cameo during the fire-in-nursery stage
}
})(),//and more components...

//Systems.AI is general and would get called for all the AI components.

//It basically spits out functions used to set events on NPC objects that
//determine their behavior. You could do it all in one shot but
//the idea is to keep it granular enough for designers to actually tweak stuff
//easily without tugging on developer pantlegs constantly.
//e.g. SuperZombies, zombies, but slightly tougher, faster, smarter
}//end npcComponents

function createNPCConstructor(npcType){

var components = npcEntities[npcType],


//objConstructor is returned but components is still accessible via closure.

objConstructor = function(){
for(var x in components){
//object iteration in

var thisComponent = components[x];

if(typeof thisComponent === 'function'){

thisComponent.apply(this);
//fires function as if it were a property of instance
//would allow the function to add additional properties and set
//event handlers via the 'this' keyword
}
else {
objConstructor.prototype[x] = thisComponent;
//public property accessed via reference to constructor prototype
//good for low memory footprint among other things
}

}
}
return objConstructor;
}

var npcBuilders= {}; //empty object literal
for (var x in npcEntities){
npcConstructors[x] = createNPCConstructor(x);
}


Now any time you need an NPC, you could build with npcBuilders.();


A GUI could plug into the npcEntities and components objects and allow designers to tweak old entities or create new entities by simply mixing and matching components (although there's no mechanism in there for non-default components but special components could be added on the fly in the code as long as there was a defined component for it.


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...