@miniplex/core
Version:
A developer-friendly entity management system for games and similarly demanding applications, based on ECS architecture.
579 lines (537 loc) • 17.6 kB
JavaScript
import { Event } from '@hmans/event';
var isArchetype = function isArchetype(entity) {
for (var _len = arguments.length, properties = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
properties[_key - 1] = arguments[_key];
}
for (var _i = 0, _properties = properties; _i < _properties.length; _i++) {
var key = _properties[_i];
if (entity[key] === undefined) return false;
}
return true;
};
var archetypeCache = new Map();
var archetype = function archetype() {
for (var _len2 = arguments.length, properties = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
properties[_key2] = arguments[_key2];
}
var normalizedProperties = properties.sort().filter(function (p) {
return !!p && p !== "";
});
var key = JSON.stringify(normalizedProperties);
if (archetypeCache.has(key)) return archetypeCache.get(key);
var predicate = function predicate(entity) {
return isArchetype.apply(void 0, [entity].concat(properties));
};
archetypeCache.set(key, predicate);
return predicate;
};
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _createForOfIteratorHelper(o, allowArrayLike) {
var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
if (!it) {
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;
var F = function () {};
return {
s: F,
n: function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
},
e: function (e) {
throw e;
},
f: F
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var normalCompletion = true,
didErr = false,
err;
return {
s: function () {
it = it.call(o);
},
n: function () {
var step = it.next();
normalCompletion = step.done;
return step;
},
e: function (e) {
didErr = true;
err = e;
},
f: function () {
try {
if (!normalCompletion && it.return != null) it.return();
} finally {
if (didErr) throw err;
}
}
};
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", {
writable: false
});
return Constructor;
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
var _Symbol$iterator;
_Symbol$iterator = Symbol.iterator;
/**
* A bucket is a collection of entities. Entities can be added, removed, and
* touched; the bucket exposes events for each of these operations.
*/
var Bucket = /*#__PURE__*/function () {
function Bucket() {
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref$entities = _ref.entities,
entities = _ref$entities === void 0 ? [] : _ref$entities;
_classCallCheck(this, Bucket);
_defineProperty(this, "entityPositions", new Map());
_defineProperty(this, "onEntityAdded", new Event());
_defineProperty(this, "onEntityRemoved", new Event());
_defineProperty(this, "onEntityTouched", new Event());
_defineProperty(this, "onCleared", new Event());
_defineProperty(this, "onDisposed", new Event());
_defineProperty(this, "derivedBuckets", new Map());
this.entities = entities;
}
/** The entities in the bucket. */
_createClass(Bucket, [{
key: _Symbol$iterator,
value: function value() {
var _this = this;
var index = this.entities.length;
return {
next: function next() {
var value = _this.entities[--index];
return {
value: value,
done: index < 0
};
}
};
}
}, {
key: "size",
get:
/**
* Returns the size of this bucket (the number of entities it contains).
*/
function get() {
return this.entities.length;
}
/**
* Returns true if this bucket is currently tracking the given entity.
*
* @param entity The entity to check for.
* @returns True if the entity is being tracked.
*/
}, {
key: "has",
value: function has(entity) {
return this.entityPositions.has(entity);
}
/**
* Adds the entity to this bucket. If the entity is already in the bucket, it
* does nothing.
*
* @param entity The entity to add.
* @returns The entity that was added.
*/
}, {
key: "add",
value: function add(entity) {
/* Add the entity if we don't already have it */
if (entity && !this.has(entity)) {
this.entities.push(entity);
this.entityPositions.set(entity, this.entities.length - 1);
this.onEntityAdded.emit(entity);
}
return entity;
}
/**
* Touches the entity, signaling this bucket that the entity was updated, and should
* be re-evaluated by any buckets derived from this one.
*
* @param entity The entity to touch.
* @returns The entity that was touched.
*/
}, {
key: "touch",
value: function touch(entity) {
if (entity && this.has(entity)) {
this.onEntityTouched.emit(entity);
}
return entity;
}
/**
* Removes the entity from this bucket. If the entity is not in the bucket,
* it does nothing.
*
* @param entity The entity to remove.
* @returns The entity that was removed.
*/
}, {
key: "remove",
value: function remove(entity) {
/* Only act if we know about the entity */
if (entity && this.has(entity)) {
/* Remove entity from our list */
var index = this.entityPositions.get(entity);
this.entityPositions["delete"](entity);
var other = this.entities[this.entities.length - 1];
if (other !== entity) {
this.entities[index] = other;
this.entityPositions.set(other, index);
}
this.entities.pop();
/* Emit event */
this.onEntityRemoved.emit(entity);
}
return entity;
}
/**
* Removes all entities from this bucket. This will emit the `onEntityRemoved` event
* for each entity, giving derived buckets a chance to remove the entity as well.
*/
}, {
key: "clear",
value: function clear() {
var _iterator = _createForOfIteratorHelper(this),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var _entity = _step.value;
this.remove(_entity);
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
this.onCleared.emit();
}
/**
* Dispose of this bucket. This will remove all entities from the bucket, dispose of all
* known derived buckets, and clear all event listeners.
*/
}, {
key: "dispose",
value: function dispose() {
/* Emit onDisposed event */
this.onDisposed.emit();
/* Clear all state */
this.derivedBuckets.clear();
this.entities = [];
this.entityPositions.clear();
this.onCleared.clear();
this.onDisposed.clear();
this.onEntityAdded.clear();
this.onEntityRemoved.clear();
this.onEntityTouched.clear();
}
/**
* Create a new bucket derived from this bucket. The derived bucket will contain
* only entities that match the given predicate, and will be updated reactively
* as entities are added, removed, or touched.
*
* @param predicate The predicate to use to filter entities.
* @returns The new derived bucket.
*/
}, {
key: "derive",
value: function derive() {
var predicate = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {
return true;
};
/* Check if we already have a derived bucket for this predicate */
var existingBucket = this.derivedBuckets.get(predicate);
if (existingBucket) return existingBucket;
/* Create bucket */
var bucket = new Bucket();
/* Add to cache */
this.derivedBuckets.set(predicate, bucket);
/* Add entities that match the predicate */
var _iterator2 = _createForOfIteratorHelper(this.entities),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var _entity2 = _step2.value;
if (predicate(_entity2)) {
bucket.add(_entity2);
}
}
/* Listen for new entities */
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
bucket.onDisposed.addListener(this.onEntityAdded.addListener(function (entity) {
if (predicate(entity)) {
bucket.add(entity);
}
}));
/* Listen for removed entities */
bucket.onDisposed.addListener(this.onEntityRemoved.addListener(function (entity) {
bucket.remove(entity);
}));
/* Listen for changed entities */
bucket.onDisposed.addListener(this.onEntityTouched.addListener(function (entity) {
if (predicate(entity)) {
bucket.add(entity);
bucket.touch(entity);
} else {
bucket.remove(entity);
}
}));
/* React to this bucket being disposed */
this.onDisposed.addListener(function () {
bucket.dispose();
});
return bucket;
}
}]);
return Bucket;
}();
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
Object.defineProperty(subClass, "prototype", {
writable: false
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
return true;
} catch (e) {
return false;
}
}
function _possibleConstructorReturn(self, call) {
if (call && (typeof call === "object" || typeof call === "function")) {
return call;
} else if (call !== void 0) {
throw new TypeError("Derived constructors may only return object or undefined");
}
return _assertThisInitialized(self);
}
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}
var World = /*#__PURE__*/function (_Bucket) {
_inherits(World, _Bucket);
var _super = _createSuper(World);
function World() {
var _this;
_classCallCheck(this, World);
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
_this = _super.call.apply(_super, [this].concat(args));
/* Forget the ID again when an entity is removed */
_defineProperty(_assertThisInitialized(_this), "nextId", 0);
_defineProperty(_assertThisInitialized(_this), "entityToId", new Map());
_defineProperty(_assertThisInitialized(_this), "idToEntity", new Map());
_this.onEntityRemoved.addListener(function (entity) {
if (_this.entityToId.has(entity)) {
_this.idToEntity["delete"](_this.entityToId.get(entity));
_this.entityToId["delete"](entity);
}
});
return _this;
}
/* ID generation */
_createClass(World, [{
key: "id",
value:
/**
* Returns the ID of the given entity. If the entity is not known to this world,
* it returns `undefined`.
*
* @param entity The entity to get the ID of.
* @returns The ID of the entity, or `undefined` if the entity is not known to this world.
*/
function id(entity) {
/* Only return IDs for entities we know about */
if (!this.has(entity)) return undefined;
/* Return existing ID if we have one */
var id = this.entityToId.get(entity);
if (id !== undefined) return id;
this.entityToId.set(entity, this.nextId);
this.idToEntity.set(this.nextId, entity);
return this.nextId++;
}
/**
* Given an ID, returns the entity with that ID. If the ID is not known to this world,
* it returns `undefined`.
*
* @param id The ID of the entity to get.
* @returns The entity with the given ID, or `undefined` if the ID is not known to this world.
*/
}, {
key: "entity",
value: function entity(id) {
return this.idToEntity.get(id);
}
/**
* Adds a component to an entity.
* If the entity already has the component, this function will do nothing.
*
* @param entity The entity to add the property to.
* @param component The component to add.
* @param value The value of the component.
* @returns `true` if the entity was updated, `false` otherwise.
*/
}, {
key: "addComponent",
value: function addComponent(entity, component, value) {
if (entity[component] !== undefined) {
console.warn("Tried to add a component, but it was already present:", component);
return false;
}
entity[component] = value;
this.touch(entity);
return true;
}
/**
* Removes a component from an entity. If the entity does not have the component,
* this function will do nothing.
*
* @param entity The entity to remove the component from.
* @param component The component to remove.
* @returns `true` if the entity was updated, `false` otherwise.
*/
}, {
key: "removeComponent",
value: function removeComponent(entity, component) {
if (entity[component] === undefined) {
console.warn("Tried to remove a component, but it was missing:", component);
return false;
}
delete entity[component];
this.touch(entity);
return true;
}
/**
* Updates the value of a component on the given entity.
* If the entity does not have the component, this function will do nothing.
*
* @param entity The entity to update.
* @param component The component to update.
* @param value The new value of the component.
* @returns `true` if the entity was updated, `false` otherwise.
*/
}, {
key: "setComponent",
value: function setComponent(entity, component, value) {
if (entity[component] === undefined) {
console.warn("Tried to set a component, but it was missing:", component);
return false;
}
entity[component] = value;
this.touch(entity);
return true;
}
}, {
key: "archetype",
value: function archetype$1() {
return this.derive(archetype.apply(void 0, arguments));
}
}]);
return World;
}(Bucket);
export { Bucket, World, archetype, isArchetype };