UNPKG

@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
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 };