Core Concepts
Actions
Actions are centralized functions for modifying the game world in a predictable manner.
Understanding Actions
Actions in Viber3D (powered by Koota) are centralized functions that modify the ECS world. They help avoid scattering spawn/remove/update logic in multiple places.
import { createActions } from 'koota'
import { IsPlayer, Health, Transform } from '../traits'
export const actions = createActions((world) => ({
spawnPlayer: (position) => {
return world.spawn(
IsPlayer(),
Transform({ position }),
Health({ amount: 100 })
)
},
destroyAllPlayers: () => {
world.query(IsPlayer).forEach((player) => {
player.destroy()
})
}
}))
Using Actions in React
With useActions
, you get your action functions bound to the current world
:
import { useActions } from 'koota/react'
import { actions } from '../actions'
function GameController() {
const { spawnPlayer, destroyAllPlayers } = useActions(actions)
useEffect(() => {
// Spawn a player on mount
spawnPlayer({ x: 0, y: 0, z: 0 })
// Cleanup on unmount
return () => destroyAllPlayers()
}, [])
return null
}
Best Practices
- Centralize: Store all your actions in a dedicated file (e.g.
src/actions.ts
oractions/
folder). - Keep them pure: Actions should only modify the ECS state; avoid large side effects.
- Descriptive names: Use clear naming like
spawnEnemyWave()
,applyDamage()
, etc. - Cleanup logic: Provide actions to remove entities or reset traits as needed.
Action Composition
Actions can call one another to build complex flows:
export const actions = createActions((world) => ({
spawnEnemy: (position) => {
return world.spawn(IsEnemy(), Transform({ position }), Health({ amount: 50 }))
},
spawnEnemyWave: (count) => {
for (let i = 0; i < count; i++) {
const position = getRandomPosition()
actions.spawnEnemy(position) // calling another action
}
}
}))
Conditional Updates
export const actions = createActions((world) => ({
damageEntity: (entity, amount) => {
if (entity.has(Health)) {
entity.set(Health, (h) => {
const newAmount = Math.max(0, h.amount - amount)
if (newAmount <= 0) {
actions.destroyEntity(entity)
}
return { ...h, amount: newAmount }
})
}
},
destroyEntity: (entity) => entity.destroy()
}))
Testing Actions
Actions are straightforward to test. You can create a test world, apply your action, and inspect the result:
describe('playerActions', () => {
let world: World
let boundActions: ReturnType<typeof actions>
beforeEach(() => {
world = createWorld()
boundActions = actions.bindTo(world)
})
it('spawnPlayer creates a player entity', () => {
const player = boundActions.spawnPlayer({ x: 0, y: 0, z: 0 })
expect(player.has(IsPlayer)).toBe(true)
expect(player.has(Health)).toBe(true)
})
it('destroyAllPlayers removes all player entities', () => {
boundActions.spawnPlayer({ x: 0, y: 0, z: 0 })
boundActions.spawnPlayer({ x: 1, y: 0, z: 0 })
boundActions.destroyAllPlayers()
expect(world.query(IsPlayer).length).toBe(0)
})
})
Summary
Actions let you organize all your ECS modifications into a single, testable location—helping maintain clarity and consistency throughout your codebase.
- Integrate Actions in React via
useActions
. - Combine with Systems for runtime logic and Traits for the data definition.
- Keep your ECS usage clean, predictable, and well-structured.