Skip to content

Releases: pmndrs/koota

v0.6.5 - Rachel's Song

06 Feb 00:59
c24159d

Choose a tag to compare

This was going to be a small release but it turns out I added three substantive features instead.

Koota agent skill

Koota now has an officially maintained agent skill. You can install it using the skills CLI, or manually copy it out of the repo.

npx skills add pmndrs/koota

It will be updated often so be sure to check for the latest version time to time. Eventually we will be able to properly version the skill.

readEach for read only query iteration

Currently the only ergonomic way to get the trait data per entity is to query and use updateEach. I found there was a lot of situations where I did not want to write, but just read the data. There is now a read only method that is a bit more performant to boot called readEach.

// This reads and writes
world.query(Position, Velocity).updateEach(([position, velocity]) => {
  position.x += velocity.x * delta
  position.y += velocity.y * delta
})

// This only reads
const data = []
world.query(Position, Velocity).readEach(([position, velocity]) => {
  data.push({ x: position.x, y: position.y })
})

You can use logical OR with Added, Removed and Changed

I had a situation recently where I needed to run a system if either position or velocity changed, it did not matter which. However, this was not supported! That has now changed along with some slight tune ups to the query internals while I was in there.

world.query(Or(Changed(Position), Changed(Velocity))).updateEach(([position, velocity]) => {
   // Do something
})

And that is it for now. Big refactors are on the way, and, with luck, an actual doc site too. Stay tuned.

What's Changed

Feat

Fix

Full Changelog: v0.6.4...v0.6.5

v0.6.4 - Agora

18 Jan 18:13

Choose a tag to compare

This is another maintenance release fixing a bunch of bugs reported thanks to @dylanhu7, mostly related to ordered relations and patterns around accessing and traversing graphs.

In addition, I took the opportunity to rename autoRemoveTarget to autoDestroy and expand its abilities. autoRemoveTarget was a misnomer as it was actually the source entity that was automatically removed, but even if this was renamed it would still not be exactly clear. I settled on autoDestroy with the options 'orphan' | 'source' | 'target', where 'orphan' and 'source' are aliases for each other. This is much more clear as the usual way we want to use this is to deal with orphans created in a graph by destroying a parent entity.

const ChildOf = relation({ autoDestroy: 'orphan' }) // Or 'source'

const parent = world.spawn()
const child = world.spawn(ChildOf(parent))
const grandchild = world.spawn(ChildOf(child))

parent.destroy()

world.has(child) // False, the child and grandchild are destroyed too

But you can now flip the relation if the relation is meant to be used in the other direction, like a Contains that points to an item where you want to destroy the target item if the entity that contains it is also destroyed.

const Contains = relation({ autoDestroy: 'target' })

const container = world.spawn()
const itemA = world.spawn()
const itemB = world.spawn()

container.add(Contains(itemA), Contains(itemB))
container.destroy()

world.has(itemA) // False, items are destroyed with container

What's Changed

Feat

  • core: Add autoDestroy behavior to relations by @krispya in #218

Fixed

  • core: Ensure entity cleanup with ordered relations and autoDestroy by @krispya in #217
  • core: Structural changes to ordered relations flag changes by @krispya in #219
  • react: useQuery could error when a relation target changes by @krispya in #220

Chore

Full Changelog: v0.6.3...v0.6.4

v0.6.3 - Dearest Alfred

07 Jan 20:31
8276a69

Choose a tag to compare

This is a bug fix release. Highlight is that the react hooks have more consistent behavior.

  • Triggering a changed event on AoS traits (objects) will properly re-render with useTrait now.
  • All hooks now properly give the new value back immediately when their target entity changes.
  • updateEach plays nice with trait modifiers like Added again.

What's Changed

Fixed

  • core: Fix lazy world trait init by @ospira in #209
  • core: Modifier trait types correctly work in updateEach by @krispya in #212
  • core: Changed modifier always works when entity generations overflow by @krispya in #213
  • react: React hooks reflect correct value when target changes by @krispya in #211
  • react: useTrait always re-renders with entity.change() by @krispya in #210

Chore

  • core: benches -> examples in pnpm-workspace.yaml by @mdirolf in #208

Full Changelog: v0.6.2...v0.6.3

v0.6.2 - Blind Curve

27 Dec 18:42

Choose a tag to compare

The highlight here is an experimental ordered relation feature. It is highly performant to get flat lists of children to iterate over, but some times explicit order is necessary. The ordered relation API provides away to get an ordered view of the children of an entity like you are used to in graphs like a Three scene.

import { relation, ordered } from 'koota'

const ChildOf = relation()
const OrderedChildren = ordered(ChildOf)

const parent = world.spawn(OrderedChildren)
const children = parent.get(OrderedChildren)

children.push(child1) // adds ChildOf(parent) to child1
children.splice(0, 1) // removes ChildOf(parent) from child1

// Bidirectional sync works both ways
child2.add(ChildOf(parent)) // child2 automatically added to list

A bidirectional sync is maintained between the bound relation and the ordered relation view letting you use an array interface or the usual relation API interchangeably. There is also an example added in the apps folder for a Hearthstone style reordering of cards in a hand. Check it out!

This is an experimental feature so please check it out and give feedback.

What's Changed

Feat

  • core: Add ordered relations for ordered lists of entities by @krispya in #201

Fix

Chore

  • examples: Update Revade for latest best practices by @krispya in #202

Full Changelog: v0.6.1...v0.6.2

v0.6.1 - La Femme d'argent

20 Dec 17:45

Choose a tag to compare

Following up the relation rewrite, events have been normalized for relations so that they are more reliable.

  • Any time a relation pair is added to an entity, it will emit an add event, ie entity.add(Likes(alice)), even if it is not first instance of that relation on the entity. Ie, the entity already has a like relation to another entity.
  • Any time a relation pair is removed from an entity, it will emit a remove event, ie entity.remove(Likes(alice)), even if it is not the last relation pair of that type on the entity.
  • A change event will only be emitted if the relation has data via the store prop and has been updated with entity.set(Likes(alice), { amount: 10 }).
  • When a relation is exclusive and a replcement is set, the old pair will get a remove event and the new pair will get an add event.

In addition, callbacks have a different signature for relations and will return entity and target instead of just entity.

const Likes = relation()

const unsub = world.onAdd(Likes, (entity, target) => {
  console.log(`Entity ${entity} likes ${target}`)
})

This allowed for introducing a two new React hooks, useTarget and useTargets.

const ChildOf = relation()

function ParentDisplay({ entity }) {
  // Returns the first target of the ChildOf relation
  const parent = useTarget(entity, ChildOf)

  if (!parent) return <div>No parent</div>

  return <div>Parent: {parent.id()}</div>
}
const Contains = relation()

function InventoryDisplay({ entity }) {
  // Returns all targets of the Contains relation
  const items = useTargets(entity, Contains)

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id()}>Item {item.id()}</li>
      ))}
    </ul>
  )
}

Some refactoring has also been underway. In an effort to consolidate and standardize the internal concepts, a ref/instance pattern was implemented. In short, a pattern emerged in Koota where a global handler called a ref gets created, such as a trait, and then it gets instantiated per-world when registered with it. As far as user-facing code goes, the biggest change is that cacheQuery has been deprecated in favor of createQuery which now follows this pattern. A query ref is returned that defines the query and also acts as a cache key. In addition there are the following changes and deprecations to type definitions in order to standardize the naming:

  • TraitData (now TraitInstance)
  • ModifierData (now Modifier)
  • Internal types like TraitInstance, QueryInstance will be removed

What's Changed

Features

  • core: Improve relation events on a per-target basis by @krispya in #198
  • react: Add useTarget and useTargets hooks by @krispya in #199

Fixes

  • react: useTrait should return undefined when the target entity becomes undefined by @mdirolf in #189
  • react: useTag and useHas return false for undefined entity by @krispya in #190
  • core: Fix tag getter by @krispya in #197

Chores

  • core: Refactor internals for explicit storage domain by @krispya in #193
  • core: Refactor world to function by @krispya in #194
  • ore: Refactor internals to follow ref/instance pattern by @krispya in #196

New Contributors

Full Changelog: v0.6.0...v0.6.1

v0.6.0 - Mattison's Independence

14 Dec 21:48

Choose a tag to compare

With this update the relations code has been entirely rewritten to be more efficient in every way, but especially with adds and removes.

A relation is made up of a pair of two entities, with the first being a subject and the second being the target. For example, entity A is a target of entity B is expressed as entityA.add(TargetOf(entityB). With the previous code, every pair would generate a new trait which made the internals simpler but much less performant. Basically, as the number of traits increased, the longer hot paths would take. Short lived relations would destroy performance after a few thousand ticks.

The new strategy only creates one trait per relation and the pairs are instead data stored on the trait. For the purposes of Koota, this is much more efficient. Both strategies are actually viable, and I may revisit the old one at some point in the future, but for now this has allowed us to greatly improve performance while keep internals extensible.

There are some breaking changes:

  • Reverse wildcard searches have been removed. It created a lot of overhead and was rarely, if ever, used. If you need to do a reverse search I suggest adding a relation in both directions. For example, ChildOf(parent) and ParentOf(child).
  • Modifiers like Changed, Removed and Added only work with the base relation ChildOf now and will not work with particular relation pairs like ChildOf(parent). This can be worked around in the future.

Stay tuned, more coming soon!

Features

feat: Optimize relations by @krispya in #178
feat: Boids relation bench by @krispya in #139
feat: Add a sim for testing relation traversal by @krispya in #179

Fixes

bug(react): useHas and useTag now return only booleans by @krispya in #184
fix: Function inlining is enabled again by @krispya in #185
fix: Relations work with modifiers by @krispya in #192
fix: Fix typo in cleanup function return statement by @pomle in #182

Chores

chore: Make the build logging more concise by @krispya in #186
chore: Use explicit type imports by @krispya in #187

Docs

chore: Remove world arg from destroyAllShips function in readme by @pomle in #181

New Contributors

Full Changelog: v0.5.3...v0.6.0

v0.5.3 - August 10

26 Nov 19:50

Choose a tag to compare

There are a few fixes here which includes making cached queries work with world.reset() and hardening the add/remove APIs when passing in multiple args. Two new hooks were also added.

useTag returns a boolean while useTrait returns a value. This is likely not what you want when using a tag in a hook.

const IsActive = trait()

function ActiveIndicator({ entity }) {
  // Returns true if the entity has the tag, false otherwise
  const isActive = useTag(entity, IsActive)

  if (!isActive) return null

  return <div>🟢 Active</div>
}

While tags are the usual need for a boolean return, you can more generically get this for any trait with the useHas hook.

const Health = trait({ amount: 100 })

function HealthIndicator({ entity }) {
  // Returns true if the entity has the trait, false otherwise
  const hasHealth = useHas(entity, Health)

  if (!hasHealth) return null

  return <div>❤️ Has Health</div>
}

What's Changed

Features
• feat: Add useTag by @krispya#176
• feat: Add useHas by @krispya#177

Fixes
• fix: Trait add and remove would early exit with variadic args by @krispya#167
• fix: Improve symbol resolution for $internal by @krispya#171
• fix: Cached queries empty after world reset by @jerzakm#172
• fix: Wildcard relations were removed early by @krispya#175

Chores
• chore: Rename TraitInstance to TraitRecord by @krispya#160
• chore: Update to new unplugin for inline plugin by @krispya#174

Docs
• docs: Fix typo in readme file by @borghiste#158
• docs: Fix typo (useStore → useStores) by @jerzakm#173

New Contributors

Full Changelog: v0.5.1...v0.5.3

v0.5.1 - Imaginary Folklore

10 Aug 18:19

Choose a tag to compare

This is mostly a maintenance release. Other than refactoring internals to get ready for bigger changes, we have an ergonomics update that has an explicit TraitsTag type for tags and a bug fix where changed detection was not working for traits changed before the query was first executed.

What's Changed

New Contributors

Full Changelog: v0.5.0...v0.5.1

v0.5.0 - Skywind Coastlines

18 Jul 16:28

Choose a tag to compare

A minor version bump means a breaking change, so what happened? Before the traits schemas were vague about using nested data structures and it was valid to add an object literal as property. However, this had unexpected results as this same object would be copied into all traits. Intuitively you might think a nested store would be created, but it turns out this makes data operations exponentially more complex and avoiding non-scalars is often considered the first rule of normalizing data.

// ❌ Arrays and objects are not allowed in trait schemas
const Inventory = trait({
  items: [],
  vec3: { x: 0, y: 0, z: 0}
  max: 10,
})

// ✅ Use a callback initializer for arrays and objects
const Inventory = trait({
  items: () => [],
  vec3: () => ({ x: 0, y: 0, z: 0})
  max: 10,
})

This breaking change should affect very few users, but all the same it is breaking. In addition to some fixes and internal cleanup we also have wildcard relation removal.

player.add(Likes(apple))
player.add(Likes(banana))

// Remove all Likes relations
player.remove(Likes('*'))

player.has(apple) // false
player.has(banana) // false

What's Changed

  • feat: constrain trait schemas to no longer accept object or array literals by @krispya in #141
  • feat: remove all relation targets with a wildcard by @krispya in #133
  • fix: world set types by @krispya in #136
  • chore: update CI node version by @krispya in #137
  • chore: update three-stdlib fixing examples by @ospira in #140

New Contributors

Full Changelog: v0.4.4...v0.5.0

v0.4.4 - Elephant Gun

13 Jun 12:12

Choose a tag to compare

I really love beautiful code. I thrive in an organized space and strive for my code to come out like a haiku, the APIs like finishing a sentence. However, performant code is often ugly and adds complexity to the mental model. For example, inlining functions is a simple way to improve performance on critical pathways that are called many times a second. Each function incurs some overhead, either in its actual execution on the machine or in the JIT compiler that needs to analyze and inline the code at runtime. At the same time, inlining functions makes it harder to maintain the code (it is in multiple places) and is also just ugly!

My ahem passion for an aesthetic dev experience drove me to write the experimental esbuild-plugin-inline-functions. This lets me write simple, legible functions but guarantee they are inlined when transpiled for the final build. To learn more about how it works, check out the repo.

While I was improving the dev experience of working on Koota, I could not stop gardening and updated all of the tooling with a monorepo layout I am testing for larger projects. It sparks joy.

But with the headline feature out of the way, here is what actually impacts users 😉 :

  • getStore is now exported. Use it to get the store. For low level use only! See docs for more info.
  • IsExcluded is now exported. This tag makes an entity excluded from all queries.
  • unpackEntity is now exported. Use this function to unpack the entity into its entity ID, generation and world ID. See docs for more info.
  • The entity generation can now be accessed with entity.generation().
  • The new linter caught some rerender edge cases in useTraitEffect and useQuery that I fixed.
  • Builds are now streamlined with automated inlining improving performance marginally.
  • Schemas now support BigInt as primitive type. If you have never seen the 1n syntax, check it out!

Note: What happened v0.4.3? I messed up publishing it, so we skipped to v0.4.4. Whoops! 🤫

What's Changed

  • chore(core): Add missing exports by @krispya in #110
  • feat(core): Entity generation method by @krispya in #118
  • feat: Use inline plugin by @krispya in #46
  • chore: Migrate to pnpm catalogs by @krispya in #122
  • chore: Switch to Oxlint by @krispya in #123
  • fix(core): Make array creation explicit by @krispya in #124
  • fix(react): useTraitEffect no longer rerenders if callback is an arrow function by @krispya in #125
  • fix(react): Make useQuery rerenders more stable by @krispya in #126
  • chore: Remove dev package and simplify dev workflow by @krispya in #127
  • feat(core): Support BigInt in schemas by @krispya in #129

Full Changelog: v0.4.2...v0.4.4