Home Reference Source

src/entity.js

  1. /** @ignore */
  2. import {
  3. invoke,
  4. shallowClone,
  5. isEntityEmpty as isEntityEmpty,
  6. } from './utilities.js'
  7.  
  8. /**
  9. * Entity class used for storing components.
  10. *
  11. * @class Entity (name)
  12. */
  13. export class Entity {
  14. /**
  15. * Do not construct an Entity yourself - use the entity() method in World instead.
  16. * Also, do not shallow/deep copy entity objects, only pass around references.
  17. *
  18. * @private
  19. *
  20. * @param {World} world - The world
  21. * @param {number} id - The identifier
  22. */
  23. constructor(world, id) {
  24. /** @ignore */
  25. this.world = world
  26.  
  27. /** @ignore */
  28. this._id = id
  29.  
  30. /** @ignore */
  31. this.data = { entity: this }
  32. }
  33.  
  34. /**
  35. * Return the entity ID.
  36. *
  37. * @return {number} Integer entity ID
  38. */
  39. get id() {
  40. return this._id
  41. }
  42.  
  43. /**
  44. * ID is read-only, attempting to set it will throw an error.
  45. *
  46. * @private
  47. *
  48. * @throws {Error} Cannot set entity id
  49. */
  50. set id(id) {
  51. throw new Error('Cannot set entity id')
  52. }
  53.  
  54. /**
  55. * Returns true if the entity has the specified component name.
  56. *
  57. * @example
  58. * entity.has('position')
  59. *
  60. * @param {string} name - The component name to check for
  61. *
  62. * @return {boolean} true or false
  63. */
  64. has(name) {
  65. return name in this.data
  66. }
  67.  
  68. /**
  69. * Returns a component by name, or undefined if it doesn't exist
  70. *
  71. * @example
  72. * const position = entity.get('position')
  73. *
  74. * @param {string} component - The component name to get
  75. *
  76. * @return {Object} The component if defined, otherwise undefined
  77. */
  78. get(component) {
  79. return this.data[component]
  80. }
  81.  
  82. /**
  83. * Returns a component by name (automatically created if it doesn't exist)
  84. * For unregistered components, passing no arguments to this will create a {} component,
  85. * if the component does not exist. The argument behavior is exactly the same as set().
  86. *
  87. * @example
  88. * const position = entity.access('position', 3, 4)
  89. *
  90. * @example
  91. * entity.access('anything').foo = 'bar'
  92. * // 'anything' now has { foo: 'bar' }
  93. *
  94. * @example
  95. * entity.access('anything', { foo: 'bar' }).foo2 = 'bar2'
  96. * // 'anything' now has { foo: 'bar', foo2: 'bar2' }
  97. *
  98. * @param {string} component - The component name to create/get
  99. * @param {...Object} [args] - The arguments to forward to create the new component, only if it doesn't exist.
  100. *
  101. * @return {Object} Always returns either the existing component, or the newly created one.
  102. */
  103. access(component, ...args) {
  104. if (!(component in this.data)) {
  105. this.set(component, ...args)
  106. }
  107. return this.data[component]
  108. }
  109.  
  110. /**
  111. * Adds a new component, or re-creates and overwrites an existing component
  112. *
  113. * @example
  114. * entity.set('position', 1, 2)
  115. * // If position was registered: { x: 1, y: 2 }
  116. * // If position was not registered: 1
  117. * // This is because only the first arg is used for unregistered components
  118. *
  119. * @example
  120. * entity.set('anonymousComponent', { keys: 'values' })
  121. *
  122. * @example
  123. * entity.set('someString', 'Any type of any value')
  124. *
  125. * @example
  126. * entity.set('thisCompIsUndefined', undefined)
  127. *
  128. * @example
  129. * entity.set('thisCompIsEmptyObject')
  130. *
  131. * @param {string} compName - The component name to create. If there is a registered component for this name,
  132. * then its constructor will be called with (...args) and an object of that type will be created. The parent
  133. * entity reference gets injected into registered components after they are constructed. The onCreate method
  134. * gets called after the component is added to the entity, and after the entity is injected. The onCreate method
  135. * also gets passed the same (...args) as the component's constructor.
  136. * @param {...Object} [args] - The arguments to forward to the registered component type. If the component type
  137. * is not registered, then only the first additional argument will be used as the value of the entire component.
  138. *
  139. * @return {Object} The original entity that set() was called on, so that operations can be chained.
  140. */
  141. set(compName, ...args) {
  142. const compClass = this.world?.entities.componentClasses[compName]
  143. if (compClass) {
  144. // Create registered component
  145. const comp = new compClass(...args)
  146. comp.entity = this
  147. this.data[compName] = comp
  148. } else if (args.length > 0) {
  149. // Use first argument as component value
  150. this.data[compName] = args[0]
  151. } else {
  152. // Set to an empty object if no args were specified
  153. this.data[compName] = {}
  154. }
  155.  
  156. // Update the index with this component
  157. if (this.valid()) {
  158. this.world.entities.addToIndex(this, compName)
  159. }
  160.  
  161. // Call custom onCreate to initialize component, and any additional arguments passed into set()
  162. invoke(this.data[compName], 'onCreate', ...args)
  163.  
  164. return this
  165. }
  166.  
  167. /**
  168. * Low level method to set components directly. It is recommended to use set() instead, unless
  169. * you know what you are doing. The onCreate method is not called, and it is expected that you
  170. * pass an already initialized component. By default, an error will be thrown if you try calling
  171. * this on a registered component, as that could have unintended consequences in your systems.
  172. * This method can be useful for saving and restoring components without serialization.
  173. *
  174. * @example
  175. * entity.replace('position', position)
  176. *
  177. * @param {string} compName - The component name to set.
  178. * @param {Object} value - Should be a previous component instance, or whatever is expected for
  179. * the component name.
  180. * @param {boolean} overwriteRegistered - Whether or not to proceed with overwriting components
  181. * in this entity that are registered components. By default, this is false.
  182. *
  183. * @return {Object} The original entity that replace() was called on, so that operations can be chained.
  184. */
  185. replace(compName, value, overwriteRegistered = false) {
  186. // Registered component check
  187. if (
  188. !overwriteRegistered &&
  189. this.world?.entities.componentClasses[compName]
  190. ) {
  191. throw new Error(
  192. `Cannot replace() component "${compName}" because it is registered. Please refer to the docs for details.`
  193. )
  194. }
  195.  
  196. // Directly set value
  197. this.data[compName] = value
  198.  
  199. // Update the index with this component
  200. if (this.valid()) {
  201. this.world.entities.addToIndex(this, compName)
  202. }
  203.  
  204. return this
  205. }
  206.  
  207. /**
  208. * Removes a component from the entity - has no effect when it doesn't exist.
  209. * Can specify an onRemove() method in your component which gets called before it is removed.
  210. * If nothing is specified, then nothing will be removed.
  211. * Attempting to remove components that aren't set will be safely ignored.
  212. *
  213. * @example
  214. * entity.remove('position')
  215. *
  216. * @param {...string} [components] - The component names to remove from the entity.
  217. *
  218. * @return {Object} The original entity that remove() was called on, so that operations can be chained.
  219. */
  220. remove(...components) {
  221. for (const name of components) {
  222. if (name in this.data) {
  223. this._removeComponent(name)
  224. }
  225. }
  226. return this
  227. }
  228.  
  229. /**
  230. * Remove this entity and all of its components from the world. After an entity is destroyed,
  231. * the object should be discarded, and it is recommended to avoid re-using it.
  232. *
  233. * @example
  234. * entity.destroy()
  235. */
  236. destroy() {
  237. if (!this.valid()) {
  238. throw new Error('Cannot destroy invalid entity')
  239. }
  240.  
  241. // Remove all components
  242. for (const name in this.data) {
  243. if (name === 'entity') continue
  244. this._removeComponent(name)
  245. }
  246.  
  247. if (!isEntityEmpty(this.data)) {
  248. throw new Error(
  249. 'Failed to remove all components. Components must have been added inside onRemove().'
  250. )
  251. }
  252.  
  253. // Remove entity from world
  254. this.world.entities.entities.delete(this._id)
  255. this.world = undefined
  256. this._id = undefined
  257. }
  258.  
  259. /**
  260. * Returns true if this is a valid, existing, and usable entity, which is attached to a world.
  261. *
  262. * @example
  263. * if (entity.valid()) {...}
  264. *
  265. * @return {boolean} true or false
  266. */
  267. valid() {
  268. // No need to actually look in the world for the ID, if entities are only ever copied by reference.
  269. // If entities are ever deep/shallow copied, this function will need to check this to be more robust.
  270. return this.world != null && this._id != null
  271. }
  272.  
  273. /**
  274. * Serializes entire entity and components to JSON.
  275. * Defining toJSON methods in your components will override the built-in behavior.
  276. *
  277. * @example
  278. * const serializedEntity = entity.toJSON()
  279. *
  280. * @return {string} JSON encoded string
  281. */
  282. toJSON() {
  283. // Don't include "entity" key in serialized data
  284. const { entity: _, ...comps } = this.data
  285. return JSON.stringify(comps)
  286. }
  287.  
  288. /**
  289. * Deserializes data from JSON, creating new components and overwriting existing components.
  290. * Defining fromJSON methods in your components will override the built-in behavior.
  291. *
  292. * @example
  293. * entity.fromJSON(serializedEntity)
  294. *
  295. * @param {string} data - A JSON string containing component data to parse, and store in this entity.
  296. *
  297. * @return {Object} The original entity that fromJSON() was called on, so that operations can be chained.
  298. */
  299. fromJSON(data) {
  300. const parsed = JSON.parse(data)
  301. for (const name in parsed) {
  302. const comp = this.access(name)
  303.  
  304. // Either call custom method or copy all properties
  305. if (typeof comp.fromJSON === 'function') {
  306. comp.fromJSON(parsed[name])
  307. } else {
  308. Object.assign(this.access(name), parsed[name])
  309. }
  310. }
  311. return this
  312. }
  313.  
  314. /**
  315. * Attaches a currently detached entity back to a world.
  316. * Do not use detached entities, get() may be safe, but avoid calling other methods
  317. * The ID will be reassigned, so do not rely on this
  318. *
  319. * @example
  320. * entity.attach(world)
  321. *
  322. * @param {World} world - The world to attach this entity to
  323. */
  324. attach(world) {
  325. if (world && !this.valid()) {
  326. // Assign new id, and reattach to world
  327. this.world = world
  328. this._id = this.world.entities.nextEntityId++
  329. this.world.entities.entities.set(this._id, this)
  330. for (const name in this.data) {
  331. if (name === 'entity') continue
  332. this.world.entities.addToIndex(this, name)
  333. }
  334. }
  335. }
  336.  
  337. /**
  338. * Removes this entity from the current world, without removing any components or data.
  339. * It can be re-attached to another world (or the same world), using the attach() method.
  340. * Do not use detached entities, get() may be safe, but avoid calling other methods
  341. * The ID will be reassigned, so do not rely on this
  342. *
  343. * @example
  344. * entity.detach()
  345. */
  346. detach() {
  347. if (this.valid()) {
  348. // Remove from current world
  349. for (const name in this.data) {
  350. if (name === 'entity') continue
  351. this.world.entities.removeFromIndex(this, name)
  352. }
  353. this.world.entities.entities.delete(this._id)
  354. this._id = undefined
  355. this.world = undefined
  356. }
  357. }
  358.  
  359. /**
  360. * Creates a copy of this entity with all of the components cloned and returns it.
  361. * Individual components are either shallow or deep copied, depending on component
  362. * registration status and if a clone() method is defined.
  363. *
  364. * @example
  365. * const clonedEntity = entity.clone()
  366. *
  367. * @example
  368. * // How to define custom clone methods
  369. * world.component('foo', class {
  370. * onCreate(bar, baz) {
  371. * this.bar = bar
  372. * this.baz = baz
  373. * this.qux = false
  374. * }
  375. * setQux(qux = true) {
  376. * this.qux = qux
  377. * }
  378. * cloneArgs() {
  379. * return [this.bar, this.baz]
  380. * }
  381. * clone(target) {
  382. * target.qux = this.qux
  383. * }
  384. * })
  385. */
  386. clone() {
  387. if (!this.valid()) {
  388. throw new Error('Cannot clone detached or invalid entity.')
  389. }
  390.  
  391. // Clone each component in this entity, to a new entity
  392. const newEntity = this.world.entity()
  393. for (const name in this.data) {
  394. if (name === 'entity') continue
  395. this._cloneComponentTo(newEntity, name)
  396. }
  397.  
  398. // Return the cloned entity
  399. return newEntity
  400. }
  401.  
  402. /**
  403. * @ignore
  404. * Clones a component from this entity to the target entity.
  405. *
  406. * @example
  407. * const source = world.entity().set('foo', 'bar')
  408. * const target = world.entity()
  409. * source._cloneComponentTo(target, 'foo')
  410. * assert(target.get('foo') === 'bar')
  411. *
  412. * @example
  413. * world.component('foo', class {
  414. * onCreate(bar, baz) {
  415. * this.bar = bar
  416. * this.baz = baz
  417. * this.qux = false
  418. * }
  419. * setQux(qux = true) {
  420. * this.qux = qux
  421. * }
  422. * cloneArgs() {
  423. * return [this.bar, this.baz]
  424. * }
  425. * clone(target) {
  426. * target.qux = this.qux
  427. * }
  428. * })
  429. * const source = world.entity()
  430. * .set('foo', 'bar', 'baz')
  431. * .set('qux', true)
  432. * const target = world.entity()
  433. * source._cloneComponentTo(target, 'foo')
  434. * assert(source.get('foo').bar === target.get('foo').bar)
  435. * assert(source.get('foo').baz === target.get('foo').baz)
  436. * assert(source.get('foo').qux === target.get('foo').qux)
  437. *
  438. * @param {Entity} targetEntity - Must be a valid entity. Could be part of another world, but it
  439. * is undefined behavior if the registered components are different types.
  440. * @param {string} name - Component name of both source and target components.
  441. *
  442. * @return {Object} The original entity that _cloneComponentTo() was called on,
  443. * so that operations can be chained.
  444. */
  445. _cloneComponentTo(targetEntity, name) {
  446. // Get component and optional arguments for cloning
  447. const component = this.get(name)
  448. const args = invoke(component, 'cloneArgs') || []
  449. const compClass = targetEntity.world.entities.componentClasses[name]
  450. if (compClass) {
  451. // Registered component, so create new using constructor, inject
  452. // entity, and call optional clone
  453. const newComponent = new compClass(...args)
  454. newComponent.entity = targetEntity
  455. targetEntity.data[name] = newComponent
  456. invoke(component, 'clone', newComponent)
  457. } else {
  458. // Unregistered component, so just shallow clone it
  459. targetEntity.data[name] = shallowClone(component)
  460. }
  461.  
  462. // Update the index with this new component
  463. targetEntity.world.entities.addToIndex(targetEntity, name)
  464.  
  465. // Call custom onCreate to initialize component, and any additional arguments passed into set()
  466. invoke(targetEntity.data[name], 'onCreate', ...args)
  467.  
  468. return this
  469. }
  470.  
  471. /** @ignore */
  472. _removeComponent(compName) {
  473. // Call custom onRemove
  474. invoke(this.data[compName], 'onRemove')
  475.  
  476. // Remove from index
  477. this.world?.entities.removeFromIndex(this, compName)
  478.  
  479. // Remove from entity
  480. delete this.data[compName]
  481. }
  482. }