elation-engine
Version:
WebGL/WebVR engine written in Javascript
577 lines (555 loc) • 20.1 kB
JavaScript
elation.require([
"engine.things.generic"
], function() {
elation.extend("engine.systems.world", function(args) {
elation.implement(this, elation.engine.systems.system);
this.children = {};
this.scene = {
'world-3d': new THREE.Scene(),
'world-dom': new THREE.Scene(),
'colliders': new THREE.Scene(),
'sky': false
};
this.persistdelay = 1000;
this.lastpersist = 0;
this.framechanges = [];
this.eventMappedObjects = new Set();
//this.scene['world-3d'].fog = new THREE.FogExp2(0x000000, 0.0000008);
//this.scene['world-3d'].fog = new THREE.FogExp2(0xffffff, 0.01);
this.system_attach = function(ev) {
console.log('INIT: world');
this.rootname = (args ? args.parentname + '/' + args.name : '/');
this.loaded = false;
this.loading = false;
if (ENV_IS_BROWSER && document.location.hash) {
this.parseDocumentHash();
elation.events.add(window, 'popstate', elation.bind(this, this.parseDocumentHash));
}
elation.events.add(this, 'world_thing_add', this);
}
this.engine_start = function(ev) {
// If no local world override, load from args
if (!this.loaded && !this.loading) {
if (!elation.utils.isEmpty(args)) {
this.load(args);
} else {
//this.createDefaultScene();
}
}
}
this.engine_frame = function(ev) {
//console.log('FRAME: world', ev);
if (!this.uniques) {
this.uniques = {};
} else {
for (var k in this.uniques) {
this.uniques[k] = false;
}
}
var uniques = this.uniques;
while (this.framechanges.length > 0) {
var changed = this.framechanges.pop();
if (!uniques[changed.id]) {
uniques[changed.id] = true;
changed.applyChanges();
}
}
}
this.engine_stop = function(ev) {
console.log('SHUTDOWN: world');
}
this.add = function(thing) {
this.attachEvents(thing);
if (!this.children[thing.name]) {
this.children[thing.name] = thing;
thing.parent = this;
if (thing.objects['3d']) {
this.scene['world-3d'].add(thing.objects['3d']);
}
if (thing.objects['dynamics']) {
this.engine.systems.physics.add(thing.objects['dynamics']);
}
if (thing.container) {
//this.renderer['world-dom'].domElement.appendChild(thing.container);
}
if (thing.colliders) {
this.scene['colliders'].add(thing.colliders);
}
elation.events.fire({type: 'world_thing_add', element: this, data: {thing: thing}});
return true;
}
return false;
}
this.attachEvents = function(thing) {
if (!this.eventMappedObjects.has(thing)) {
elation.events.add(thing, 'thing_add,thing_remove,thing_change,thing_change_queued', this);
this.eventMappedObjects.add(thing);
}
if (thing.children) {
for (var k in thing.children) {
this.attachEvents(thing.children[k]);
}
}
}
this.removeEvents = function(thing) {
if (this.eventMappedObjects.has(thing)) {
elation.events.remove(thing, 'thing_add,thing_remove,thing_change,thing_change_queued', this);
if (thing.children) {
for (var k in thing.children) {
this.removeEvents(thing.children[k]);
}
}
this.eventMappedObjects.delete(thing);
}
}
this.thing_add = function(ev) {
//elation.events.fire({type: 'world_thing_add', element: this, data: ev.data});
this.attachEvents(ev.data.thing);
//if (this.hasLights(ev.data.thing)) {
// this.refreshLights();
//}
}
this.thing_remove = function(ev) {
this.removeEvents(ev.data.thing);
elation.events.fire({type: 'world_thing_remove', element: this, data: ev.data});
elation.events.remove(ev.data.thing, 'thing_add,thing_remove,thing_change', this);
}
this.thing_change = function(ev) {
elation.events.fire({type: 'world_thing_change', element: this, data: ev.data});
}
this.thing_change_queued = function(ev) {
var thing = ev.target;
if (thing) {
this.framechanges.push(thing);
this.engine.systems.render.setdirty();
}
}
this.world_thing_add = function(ev) {
//elation.events.add(ev.data.thing, 'thing_add,thing_remove,thing_change', this);
this.attachEvents(ev.data.thing);
//if (this.hasLights(ev.data.thing)) {
// this.refreshLights();
//}
}
this.remove = function(thing) {
if (this.children[thing.name]) {
if (thing.objects['3d']) {
this.scene['world-3d'].remove(thing.objects['3d']);
}
if (thing.container) {
//this.renderer['world-dom'].domElement.removeChild(thing.container);
}
if (thing.colliders) {
this.scene['colliders'].remove(thing.colliders);
}
delete this.children[thing.name];
elation.events.fire({type: 'world_thing_remove', element: this, data: {thing: thing}});
}
}
this.extract_types = function(things, types, onlymissing) {
if (!types) {
types = [];
}
if (!elation.utils.isArray(things)) {
things = [things];
}
for (var i = 0; i < things.length; i++) {
var thing = things[i];
if (((onlymissing && typeof elation.engine.things[thing.type] == 'undefined') || !onlymissing) && types.indexOf(thing.type) == -1) {
types.push(thing.type);
elation.engine.things[thing.type] = null;
}
if (thing.things) {
for (var k in thing.things) {
this.extract_types(thing.things[k], types, onlymissing);
}
}
}
return types;
}
this.reset = function() {
// Kill all objects except ones which are set to persist
for (var k in this.children) {
if (!this.children[k].properties.persist) {
this.children[k].die();
}
}
while (this.scene['world-3d'].children.length > 0) {
this.scene['world-3d'].remove(this.scene['world-3d'].children[0]);
}
while (this.scene['colliders'].children.length > 0) {
this.scene['colliders'].remove(this.scene['colliders'].children[0]);
}
// Initialize collider scene with some basic lighting for debug purposes
this.scene['colliders'].add(new THREE.AmbientLight(0xcccccc));
var colliderlight = new THREE.DirectionalLight();
colliderlight.position.set(10, 17.5, 19);
this.scene['colliders'].add(colliderlight);
}
this.createNew = function() {
this.reset();
this.spawn("sector", "default");
}
this.saveLocal = function(name) {
if (!name) name = this.rootname;
console.log('Saved local world: ' + name);
var key = 'elation.engine.world.override:' + name;
localStorage[key] = JSON.stringify(this.serialize());
}
this.loadLocal = function(name) {
console.log('Load local world: ' + name);
this.rootname = name;
this.reset();
var key = 'elation.engine.world.override:' + name;
if (localStorage[key]) {
var world = JSON.parse(localStorage[key]);
this.load(world);
} else {
this.createDefaultScene();
}
if (ENV_IS_BROWSER) {
var hashargs = elation.url();
hashargs['world.load'] = name;
if (this.engine.systems.physics.timescale == 0) {
hashargs['world.paused'] = 1;
}
document.location.hash = elation.utils.encodeURLParams(hashargs);
}
}
this.listLocalOverrides = function() {
var overrides = [];
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
if (key.match(/elation\.engine\.world\.override:/)) {
var name = key.substr(key.indexOf(':')+1);
overrides.push(name);
}
}
return overrides;
}
this.load = function(things, root, logprefix) {
if (!things) return;
if (!elation.utils.isArray(things)) {
things = [things];
}
this.loading = true;
if (!this.root) {
this.currentlyloaded = thing;
var loadtypes = this.extract_types(things, [], true);
if (loadtypes.length > 0) {
elation.require(loadtypes.map(function(a) { return 'engine.things.' + a; }), elation.bind(this, function() { this.load(things, root, logprefix); }));
return;
}
}
if (!logprefix) logprefix = "";
if (typeof root == 'undefined') {
//this.rootname = (thing.parentname ? thing.parentname : '') + '/' + thing.name;
root = this;
}
for (var i = 0; i < things.length; i++) {
var thing = things[i];
var currentobj = this.spawn(thing.type, thing.name, thing.properties, root, false);
if (thing.things) {
for (var k in thing.things) {
this.load(thing.things[k], currentobj, logprefix + "\t");
}
}
}
if (root === this) {
this.loaded = true;
this.loading = false;
this.dirty = true;
elation.events.fire({type: 'engine_world_init', element: this});
}
return root;
}
this.reload = function() {
if (this.rootname) {
this.loadLocal(this.rootname);
}
}
this.refresh = function() {
elation.events.fire({type: 'world_change', element: this});
}
this.hasLights = function(thing) {
if (!thing.objects['3d']) {
console.warn('Thing has no object!', thing);
return false;
}
var object = thing.objects['3d'];
var hasLight = object instanceof THREE.Light;
if (!hasLight && object.children.length > 0) {
object.traverse(function(n) { if (n instanceof THREE.Light) { hasLight = true; } });
}
return hasLight;
}
this.refreshLights = function() {
// Not needed anymore since Three.js does this automatically now
//this.scene['world-3d'].traverse(function(n) { if (n instanceof THREE.Mesh) { n.material.needsUpdate = true; } });
}
this.createDefaultScene = function() {
var scenedef = {
type: 'sector',
name: 'default',
properties: {
persist: true
},
things: {
ground: {
type: 'terrain',
name: 'ground',
properties: {
'textures.map': '/media/space/textures/dirt.jpg',
'textures.normalMap': '/media/space/textures/dirt-normal.jpg',
'textures.mapRepeat': [ 100, 100 ],
'persist': true,
'position': [0,0,100]
}
},
sun: {
type: 'light',
name: 'sun',
properties: {
type: 'directional',
position: [ -20, 50, 25 ],
persist: true
}
}
}
};
this.load(scenedef);
}
this.loadSceneFromURL = function(url, callback) {
//this.reset();
elation.net.get(url, null, { onload: elation.bind(this, this.handleSceneLoad, callback) });
if (ENV_IS_BROWSER) {
var dochash = "world.url=" + url;
if (this.engine.systems.physics.timescale == 0) {
dochash += "&world.paused=1";
}
document.location.hash = dochash;
}
}
this.handleSceneLoad = function(callback, ev) {
var response = ev.target.response;
var data = JSON.parse(response);
if (elation.utils.isArray(data)) {
for (var i = 0; i < data.length; i++) {
this.load(data[i]);
}
} else {
this.load(data);
}
if (callback) { setTimeout(callback, 0); }
}
this.spawn = function(type, name, spawnargs, parent, autoload) {
if (elation.utils.isNull(name)) name = type + Math.floor(Math.random() * 1000000);
if (!spawnargs) spawnargs = {};
//if (!parent) parent = this.children['default'] || this;
if (typeof autoload == 'undefined') autoload = true;
var logprefix = "";
var currentobj = false;
var realtype = type;
var initialized = false;
try {
if (typeof elation.engine.things[type] != 'function') {
if (autoload) {
// Asynchronously load the new object type's definition, and create the real object when we're done
elation.require('engine.things.' + realtype, elation.bind(this, function() {
if (currentobj) {
currentobj.die();
}
this.spawn(realtype, name, spawnargs, parent, false);
}));
}
// FIXME - we should be able to return a generic, load the new object asynchronously, and then morph the generic into the specified type
// Right now this might end up with weird double-object behavior...
type = 'generic';
} else {
currentobj = elation.engine.things[type].obj[name];
if (currentobj) {
for (var k in spawnargs) {
currentobj.setProperties(spawnargs);
}
} else {
currentobj = elation.engine.things[type]({type: realtype, container: elation.html.create(), name: name, engine: this.engine, client: this.client, properties: spawnargs});
}
if (parent) {
parent.add(currentobj);
//currentobj.reparent(parent);
}
//console.log(logprefix + "\t- added new " + type + ": " + name, currentobj);
}
} catch (e) {
console.error(e.stack);
}
return currentobj;
}
this.serialize = function(serializeAll) {
var ret = {};
for (var k in this.children) {
if (this.children[k].properties.persist) {
ret[k] = this.children[k].serialize(serializeAll);
return ret[k]; // FIXME - dumb
}
}
return null;
}
this.setSky = function(texture, format, prefixes) {
if (texture !== false) {
if (!(texture instanceof THREE.Texture)) {
format = format || 'jpg';
prefixes = prefixes || ['p', 'n'];
if (texture.substr(texture.length-1) != '/') {
texture += '/';
}
var urls = [
texture + prefixes[0] + 'x' + '.' + format, texture + prefixes[1] + 'x' + '.' + format,
texture + prefixes[0] + 'y' + '.' + format, texture + prefixes[1] + 'y' + '.' + format,
texture + prefixes[0] + 'z' + '.' + format, texture + prefixes[1] + 'z' + '.' + format
];
var texturecube = THREE.ImageUtils.loadTextureCube( urls, undefined, elation.bind(this, this.refresh) );
texturecube.format = THREE.RGBFormat;
this.skytexture = texturecube;
} else {
this.skytexture = texture;
}
if (!this.scene['sky']) {
this.scene['sky'] = (this.engine.systems.render && this.engine.systems.render.views[0] ? this.engine.systems.render.views[0].skyscene : new THREE.Scene());
var skygeom = new THREE.BoxGeometry(1,1,1, 10, 10, 10);
var skymat = new THREE.MeshBasicMaterial({color: 0xff0000, side: THREE.DoubleSide, wireframe: true, depthWrite: false});
this.skyshader = THREE.ShaderLib[ "cube" ];
var skymat = new THREE.ShaderMaterial( {
fragmentShader: this.skyshader.fragmentShader,
vertexShader: this.skyshader.vertexShader,
uniforms: this.skyshader.uniforms,
depthWrite: false,
side: THREE.DoubleSide
} );
this.skymesh = new THREE.Mesh(skygeom, skymat);
this.scene['sky'].add(this.skymesh);
//console.log('create sky mesh', this.scene['sky'], this.engine.systems.render.views['main']);
if (this.engine.systems.render && this.engine.systems.render.views['main']) {
this.engine.systems.render.views['main'].setskyscene(this.scene['sky']);
}
}
this.skyshader.uniforms[ "tCube" ].value = this.skytexture;
this.skyenabled = true;
} else {
this.skyenabled = false;
}
if (this.skyenabled) {
}
}
this.setClearColor = function(color, opacity) {
this.engine.systems['render'].setclearcolor(color, opacity);
}
this.setFog = function(near, far, color) {
if (typeof color == 'undefined') color = 0xffffff;
this.scene['world-3d'].fog = new THREE.Fog(color, near, far);
}
this.setFogExp = function(exp, color) {
if (!color) color = 0xffffff;
this.scene['world-3d'].fog = new THREE.FogExp2(color, exp);
}
this.disableFog = function() {
this.scene['world-3d'].fog = false;
}
this.parseDocumentHash = function() {
var parsedurl = elation.utils.parseURL(document.location.hash);
if (parsedurl.hashargs) {
if (+parsedurl.hashargs['world.paused']) {
this.engine.systems.physics.timescale = 0;
}
if (parsedurl.hashargs['world.load'] && parsedurl.hashargs['world.load'] != this.rootname) {
this.loadLocal(parsedurl.hashargs['world.load']);
}
if (parsedurl.hashargs['world.url']) {
elation.net.get(parsedurl.hashargs['world.url'], null, {
callback: function(response) {
try {
var data = JSON.parse(response);
this.load(data);
} catch (e) {
console.log('Error loading world:', response, e);
}
}.bind(this)
});
}
}
}
// Convenience functions for querying objects from world
this.getThingsByTag = function(tag) {
var things = [];
var childnames = Object.keys(this.children);
for (var i = 0; i < childnames.length; i++) {
var childname = childnames[i];
if (this.children[childname].hasTag(tag)) {
things.push(this.children[childname]);
}
this.children[childname].getChildrenByTag(tag, things);
}
return things;
}
this.getThingsByPlayer = function(player) {
var things = [];
for (var k in this.children) {
if (this.children[k].getPlayer() == player) {
things.push(this.children[k]);
}
this.children[k].getChildrenByPlayer(player, things);
}
return things;
}
this.getThingsByType = function(type) {
var things = [];
var childnames = Object.keys(this.children);
for (var i = 0; i < childnames.length; i++) {
var childname = childnames[i];
if (this.children[childname].type == type) {
things.push(this.children[childname]);
}
this.children[childname].getChildrenByType(type, things);
}
return things;
}
this.getThingsByProperty = function(key, value) {
var things = [];
var childnames = Object.keys(this.children);
for (var i = 0; i < childnames.length; i++) {
var childname = childnames[i];
if (this.children[childname][key] === value) {
things.push(this.children[childname]);
}
this.children[childname].getChildrenByProperty(key, value, things);
}
return things;
}
this.getThingByObject = function(obj) {
}
this.getThingById = function(id) {
}
this.worldToLocal = function(pos) {
return pos;
}
this.localToWorld = function(pos) {
return pos;
}
this.worldToLocalOrientation = function(orient) {
return orient;
}
this.localToWorldOrientation = function(orient) {
return orient;
}
this.enableDebug = function() {
for (let k in this.children) {
this.children[k].enableDebug();
}
}
this.disableDebug = function() {
for (let k in this.children) {
this.children[k].disableDebug();
}
}
});
});