Releases: pmndrs/koota
v0.6.5 - Rachel's Song
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/kootaIt 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
- feat: add Koota agent skill by @krispya in #228
- feat: Add
readEachto query results by @krispya in #223 - feat: Tracking modifiers with logical
ORby @krispya in #225
Fix
Full Changelog: v0.6.4...v0.6.5
v0.6.4 - Agora
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 tooBut 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 containerWhat's Changed
Feat
Fixed
- core: Ensure entity cleanup with ordered relations and
autoDestroyby @krispya in #217 - core: Structural changes to ordered relations flag changes by @krispya in #219
- react:
useQuerycould error when a relation target changes by @krispya in #220
Chore
Full Changelog: v0.6.3...v0.6.4
v0.6.3 - Dearest Alfred
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
useTraitnow. - All hooks now properly give the new value back immediately when their target entity changes.
updateEachplays nice with trait modifiers likeAddedagain.
What's Changed
Fixed
- core: Fix lazy world trait init by @ospira in #209
- core: Modifier trait types correctly work in
updateEachby @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:
useTraitalways re-renders withentity.change()by @krispya in #210
Chore
Full Changelog: v0.6.2...v0.6.3
v0.6.2 - Blind Curve
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 listA 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
Fix
- core: Tag type by @krispya in #204
- core: Query updateEach works if the param order changes by @krispya in #206
- core: Queries no longer eagerly register by @krispya in #207
Chore
Full Changelog: v0.6.1...v0.6.2
v0.6.1 - La Femme d'argent
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:
useTaganduseHasreturnfalsefor 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
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)andParentOf(child). - Modifiers like
Changed,RemovedandAddedonly work with the base relationChildOfnow and will not work with particular relation pairs likeChildOf(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
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
- @borghiste made their first contribution in #158
- @jerzakm made their first contribution in #173
Full Changelog: v0.5.1...v0.5.3
v0.5.1 - Imaginary Folklore
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
- fix typo into README by @Cosmitar in #142
- chore: update revade example by @ospira in #143
- refactor: remove TraitData class by @krispya in #145
- refactor: remove ModifierData class by @krispya in #146
- refactor: remove entity cleanup from World.destroy() by @r04423 in #144
- refactor: remove Query class, streamline internal usage by @krispya in #147
- fix: update setChanged logic to correctly populate changedMask by @r04423 in #149
- fix: Tag trait inference by @iwoplaza in #152
- refactor: Reuse default tag schema by @krispya in #154
New Contributors
Full Changelog: v0.5.0...v0.5.1
v0.5.0 - Skywind Coastlines
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) // falseWhat'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
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 😉 :
getStoreis now exported. Use it to get the store. For low level use only! See docs for more info.IsExcludedis now exported. This tag makes an entity excluded from all queries.unpackEntityis 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
useTraitEffectanduseQuerythat I fixed. - Builds are now streamlined with automated inlining improving performance marginally.
- Schemas now support
BigIntas primitive type. If you have never seen the1nsyntax, 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