/**
* Game Engine
*
* The game engine main class
*
* @class engine
* @namespace zerk.game
* @extends zerk.observable
* @module zerk
*/
zerk.define({
name: 'zerk.game.engine',
extend: 'zerk.observable',
require: [
'zerk.game.engine.registry',
'zerk.game.engine.entityLoader',
'zerk.game.engine.componentLoader',
'zerk.game.engine.worldLoader'
]
},{
/**
* Game engine registry
*
* @property _registry
* @type zerk.game.engine.registry
* @protected
**/
_registry: null,
/**
* JSON loader instance
*
* @property _jsonLoader
* @type zerk.jsonLoader
* @protected
**/
_jsonLoader: null,
/**
* Component loader instance
*
* @property _componentLoader
* @type zerk.game.engine.componentLoader
* @protected
**/
_componentLoader: null,
/**
* Entity loader instance
*
* @property _entityLoader
* @type zerk.game.engine.entityLoader
* @protected
**/
_entityLoader: null,
/**
* World loader instance
*
* @property _worldLoader
* @type zerk.game.engine.worldLoader
* @protected
**/
_worldLoader: null,
/**
* Engine configuration
*
* @property _config
* @type Object
* @protected
**/
_config: null,
/**
* Total world runtime
*
* @property _time
* @type Integer
* @protected
**/
_time: 0,
/**
* World timer interval
*
* @property _worldInterval
* @type Float
* @protected
**/
/*
* TODO Make the world interval configurable
*/
_worldInterval: 1000/60,
/**
* World timer
*
* @property _timer
* @type DOMTimer
* @protected
**/
_timer: null,
/**
* Indicates that the engine is running
*
* @property _running
* @type Boolean
* @protected
**/
_running: false,
/**
* Indicates that the world is unloading
*
* @property _unloadingWorld
* @type Boolean
* @protected
* @deprecated
**/
_unloadingWorld: false,
/**
* Entities register
*
* @property _entities
* @type Array
* @protected
**/
_entities: null,
/**
* Entity id map
*
* @property _entityIdMap
* @type Object
* @protected
**/
_entityIdMap: null,
/**
* System register
*
* @property _system
* @type Object
* @protected
**/
_system: null,
/**
* Entities grouped by threads for fast access
*
* @property _threadMap
* @type Object
* @protected
**/
_threadMap: null,
/**
* Map of systems by names
*
* @property _systemMap
* @type Object
* @protected
**/
_systemMap: null,
/**
* Last used entity id
*
* Used to generate the next entity id.
*
* @property _lastEntityId
* @type Integer
* @protected
**/
_lastEntityId: 0,
/* --- ENGINE --- */
/**
* Class constructor
*
* @method init
* @param {zerk.jsonLoader} jsonLoader A JSON Loader instance
* @param {Object} config Game configuration
**/
init: function(jsonLoader,config) {
zerk.parent('zerk.game.engine').init.apply(
this,
arguments
);
this._entityIdMap={};
this._entities=[];
this._system={};
this._threadMap={};
this._systemMap={};
this._jsonLoader=jsonLoader;
this._registry=zerk.create(
'zerk.game.engine.registry',
config // Feed up the registry with the initial user config object
);
this._config={
componentMap: {},
systemMap: {},
defaultSystems: [],
version: '0.1.0'
};
this._config=this._registry.register('engine',this._config);
// Setup loaders
this._componentLoader=zerk.create(
'zerk.game.engine.componentLoader',
this._jsonLoader,
this._config.componentMap
);
this._entityLoader=zerk.create(
'zerk.game.engine.entityLoader',
this._jsonLoader,
this._componentLoader
);
this._worldLoader=zerk.create(
'zerk.game.engine.worldLoader',
this._jsonLoader,
this._componentLoader,
this._entityLoader
);
// Setup renderer thread
// Setup crossbrowser requestAnimationFrame
zerk.browser.setupRequestAnimationFrame();
var self=this;
(function animloop() {
requestAnimationFrame(animloop);
self._render();
})();
this._log('Init');
},
/**
* Start game engine
*
* @method start
**/
start: function() {
this._log('Started');
this._running=true;
this._tick(); // Run the first tick
this._startTimer();
return true;
},
/**
* Stop game engine
*
* @method stop
**/
stop: function() {
if (!this._running) return;
this._log('Stopping');
this._stopTimer();
this._running=false;
this.reset();
this._log('Stopped (Time '+this._time+')');
return true;
},
/**
* Pause game engine
*
* @method pause
**/
pause: function() {
this._stopTimer();
this._running=false;
this._log('Game paused (Time '+this._time+')');
return true;
},
/**
* Loads a world
*
* @method loadWorld
* @param {String} worldClass Resource id of the world to be loaded
* @param {Function} successHandler Event handler for success
* @param {Function} errorHandler Event handler for error
* @async
**/
loadWorld: function(name,successHandler,errorHandler) {
var self=this;
this._log('Loading world "'+name+'"');
this._log('Loading world resources',2);
this._worldLoader.loadWorld(
name,
function (data) {
self._onLoadWorld(data,successHandler,errorHandler);
},
function (error) {
errorHandler(error);
console.log('World Load error:',error);
}
);
},
/**
* Reset the engine
*
* @method reset
**/
reset: function() {
this._log('Unloading world');
this._log('Clear entities',2);
this.clearEntities();
this._log('Stop systems',2);
this.stopSystems();
this._log('Clear systems',2);
this.clearSystems();
this._unloadingWorld=false;
this.fireEvent('worldunloaded');
},
/**
* Save world state
*
* @method saveWorld
* @return {String} World state as JSON string
**/
saveWorld: function() {
/*
* TODO Recreate save method
*/
/*
var data={};
data.worldConfig=this.world._config;
data.entities=[];
for (var i=0;i<this._entities.length;i++) {
data.entities.push({
name: this._entities[i].name,
config: this._entities[i].config
});
}
var json=JSON.stringify(data);
return json;
*/
},
/**
* Returns true if the game engine is running
*
* @method isRunning
* @return {Boolean} True if the game engine is running
**/
isRunning: function() {
return this._running;
},
/**
* Returns current world time
*
* @method getTime
* @return {Integer} World time
**/
getTime: function() {
return this._time;
},
/**
* Returns the game engine registry
*
* @method getRegistry
* @return {zerk.game.engine.registry} Returns the registry instance
**/
getRegistry: function() {
return this._registry;
},
/**
* Loads given entities
*
* @method loadEntities
* @param {Array} entities An array of entity resource ids
* @param {Function} successHandler Event handler for success
* @param {Function} errorHandler Event handler for error
* @async
**/
loadEntities: function(entities,successHandler,errorHandler) {
return this._entityLoader.loadEntities(
entities,
successHandler,
errorHandler
);
},
/* --- ENTITY MANAGER --- */
/**
* Creates a new entity instance with given configuration
*
* @method addEntity
* @param {config.entity} config Entity configuration
**/
addEntity: function(config) {
/**
* *** THIS IS NOT A CLASS! ITS A CONFIGURATION OBJECT. ***
*
* Entity configuration.
*
* Used by {{#crossLink "zerk.game.engine"}}{{/crossLink}}
*
* @class config.entity
**/
var extendedConfig={
/**
* Unqiue entity id
*
* @property id
* @type Integer
**/
id: null,
/**
* Name of the entity
*
* @property name
* @type String
**/
name: '',
/**
* Tags assigned to this entity
*
* @property tags
* @type Array
**/
tags: [],
/**
* Components contained in the entity
*
* @property components
* @type Object
**/
components: {}
};
zerk.apply(extendedConfig,config);
// Entity definition
var definition=this._entityLoader.getEntity(extendedConfig.name);
if (!definition) {
zerk.error({
message: 'Entity is not loaded "'+extendedConfig.name+'"'
});
}
// Local config
var entity=this._componentLoader.buildComponents(definition,extendedConfig);
// Generate ID
this._lastEntityId++;
entity.id=this._lastEntityId;
// Get list of systems intereste in this entity
var systemList=this.getEntitySystemList(entity);
// Add the entity to related systems
for (var i=0;i<systemList.length;i++) {
this._system[systemList[i]].addEntity(entity);
}
// Add entity to the world register
this._entities.push(entity);
// Create ID map entry
this._entityIdMap['id'+entity.id]=entity;
this._log('Spawned "'+entity.name+'"'+' id "'+entity.id+'"',4);
},
/**
* Removes given entity instance from the world
*
* @for zerk.game.engine
* @method removeEntity
* @param {config.entity} Entity state
* @return {Boolean} Returns true on success
**/
removeEntity: function(entity) {
// Get list of systems intereste in this entity
var systemList=this.getEntitySystemList(entity);
for (var i=0;i<systemList.length;i++) {
this._system[systemList[i]].removeEntity(entity);
}
delete this._entityIdMap['id'+entity.id];
for (var i=0;i<this._entities.length;i++) {
if (this._entities[i].id==entity.id) {
this._entities.splice(i,1);
this._log('Destroyed "'+entity.id+'"',4);
return true;
}
}
return false;
},
/**
* Returns entity instance by given id
*
* @method getEntityById
* @param {String} id Entity id
* @return {null|config.entity} Entity or null
**/
getEntityById: function(id) {
if (!zerk.isDefined(this._entityIdMap['id'+id])) return null;
return this._entityIdMap['id'+id];
},
/**
* Returns entites that contain all given tags
*
* @method getEntitiesByTags
* @param {Array} tags Array of tags
* @return {Array} Array of entities
**/
/*
* TODO Find performant way to query entities by tags
*/
getEntitiesByTags: function(tags) {
// Force into array
if (!zerk.isArray(tags)) {
tags=[tags];
}
var result=[];
for (var i=0;i<this._entities.length;i++) {
if (zerk.isDefined(this._entities[i].tags)
&& zerk.isArray(this._entities[i].tags)
&& !zerk.isEmpty(this._entities[i].tags)) {
var match=true;
for (var c=0;c<tags.length;c++) {
if (!zerk.inArray(tags[c],this._entities[i].tags)) {
match=false;
break;
}
}
if (match) {
result.push(this._entities[i]);
}
}
}
return result;
},
/**
* Removes all entities from the world
*
* @method clearEntities
**/
clearEntities: function() {
this._log('Clear');
while (this._entities.length>0) {
this.removeEntity(this._entities[0],true);
}
// Reset ID counter
this._lastEntityId=0;
},
/* --- SYSTEM MANAGER --- */
/**
* Adds a system to the engine
*
* @method addSystem
* @param {String} name Name of the system. Not the class name.
* @param {Object} config Initial config for the system
* @return {Boolean} True on success
**/
addSystem: function(name,config) {
this._log('Add system "'+name+'"');
if (this.isSystemRegistered(name)) return;
var systemClass=this._getSystemClass(name);
var system=zerk.create(
systemClass,
this,
config
);
var thread=system.getThread();
// Register the system under its name
this._system[system.getName()]=system;
// Create a thread map entry
if (typeof this._threadMap[thread]=='undefined') {
this._threadMap[thread]=[];
}
var inserted=false;
for (var i=0;i<this._threadMap[thread];i++) {
if (this._threadMap[thread][i].getPriority()<system.getPriority()) {
this._threadMap[thread].splice(i-1,0,system);
inserted=true;
}
}
if (!inserted) {
this._threadMap[thread].push(system);
}
return system;
},
/**
* Removes a system from the engine
*
* @method removeSystem
* @param {String} name Name of the system. Not the class name.
* @return {Boolean} True on success
**/
removeSystem: function(name) {
this._log('Remove system "'+name+'"');
if (!this.isSystemRegistered(name)) return;
var system=this.getSystem(name);
var thread=system.getThread();
// Remove register entry
for (var key in this._system) {
if (key==name) {
delete this._system[key];
break;
}
}
// Remove thread map entry
for (var i=0;i<this._threadMap[thread].length;i++) {
if (this._threadMap[thread][i].getName()==name) {
this._threadMap[thread].splice(i,1);
break;
}
}
return true;
},
/**
* Removes all system from the engine
*
* @method clearSystems
**/
clearSystems: function() {
this._log('Clearing...');
for (var system in this._system) {
this.removeSystem(system);
}
this._log('Cleared');
},
/**
* Starts all systems
*
* @method startSystems
**/
startSystems: function() {
var i=0;
for (var thread in this._threadMap) {
for (i=0;i<this._threadMap[thread].length;i++) {
this._threadMap[thread][i].start();
}
}
},
/**
* Stops all systems
*
* @method stopSystems
**/
stopSystems: function() {
var i=0;
for (var thread in this._threadMap) {
for (i=this._threadMap[thread].length-1;i>=0;i--) {
this._threadMap[thread][i].stop();
}
}
},
/**
* Returns a list of systems interested in given entity
*
* @method getEntitySystemList
* @param {config.entity} entity Entity state
* @return {Array} Array of systems
**/
getEntitySystemList: function(entity) {
var keys={};
for (var component in entity.components) {
for (var system in this._system) {
if (this._system[system].useComponent(component)) {
keys[system]=true;
}
}
}
var result=[];
for (var system in keys) {
result.push(system);
}
return result;
},
/**
* Returns a system instance
*
* @method getSystem
* @param {String} name System name. Not the class name.
* @return {zerk.game.engine.system} System instance
**/
getSystem: function(name) {
if (!zerk.isDefined(this._system[name])) {
return false;
}
return this._system[name];
},
/**
* Returns true when given system is loaded in the engine
*
* @method isSystemRegistered
* @param {String} name System name. Not the class name.
* @return {Boolean} True when the system is loaded
**/
isSystemRegistered: function(name) {
return (typeof this._system[name]!='undefined');
},
/**
* Updates all systems in given thread
*
* @method _updateSystems
* @param {String} thread Thread name
* @protected
**/
_updateSystems: function(thread) {
if (typeof this._threadMap[thread]=='undefined') return;
//console.log('UPDATE '+thread);
// Process all systems in given thread register
for (var i=0;i<this._threadMap[thread].length;i++) {
this._threadMap[thread][i].update();
}
},
/**
* Returns the class name for given system name
*
* @method _getSystemClass
* @param {String} name System name. Not the class name.
* @return {String} Class name of the system
* @protected
**/
_getSystemClass: function(name) {
if (typeof this._config.systemMap[name]=='undefined') return false;
return this._config.systemMap[name];
},
/**
* Fires when the world definition is laoded
*
* @method _onLoadWorld
* @param {Object} world World definition
* @param {Function} successHandler Event handler for success
* @param {Function} errorHandler Event handler for error
* @protected
**/
_onLoadWorld: function(world,successHandler,errorHandler) {
// Merge list of default systems with world systems
var systems=[];
var defaultSystems=this._config.defaultSystems;
for (var i=0;i<defaultSystems.length;i++) {
systems.push(defaultSystems[i]);
}
for (var system in world.config.systems) {
systems.push(system);
}
systems=zerk.arrayUnique(systems);
// Add systems
this._log('Loading systems ('+systems.length+')',2);
var systemConfig=null;
for (var i=0;i<systems.length;i++) {
systemConfig={};
if (typeof world.config.systems[systems[i]]!='undefined') {
systemConfig=world.config.systems[systems[i]];
}
this.addSystem(systems[i],systemConfig);
}
// Start systems
this._log('Starting systems',2);
this.startSystems();
// Add entities
this._log('Loading entities ('+world.entities.length+')',2);
for (var i=0;i<world.entities.length;i++) {
/*
* TODO Check why world is modified in here
*/
this.addEntity(world.entities[i]);
}
// Execute callback
successHandler();
this._log('Loaded world');
},
/**
* Starts the engine timer
*
* @method _startTimer
* @protected
**/
_startTimer: function() {
var self=this;
this._timer=window.setInterval(
function() {
self._tick();
},
this._worldInterval
);
},
/**
* Stops the engine timer
*
* @method _stopTimer
* @protected
**/
_stopTimer: function() {
window.clearInterval(this._timer);
},
/**
* Game engine simulation tick
*
* @method _tick
* @protected
**/
_tick: function() {
this._time++;
this._updateSystems('simulation');
},
/**
* Game engine render tick
*
* @method _render
* @protected
**/
_render: function() {
if (!this._running) return;
this._updateSystems('render');
},
/**
* Local log method
*
* @method _log
* @param {String} message Log message
* @param {Integer} severity Log severity
* @protected
**/
_log: function(message,severity) {
zerk.log({
message: message,
group: 'Engine',
severity: severity
});
}
});