Systems
Systems are the functions that run every tick. Systems must adhere to the following function signature:
type System func(cardinal.WorldContext) error
Systems are given a WorldContext object which can be used to iterate over transactions, modify component values, search for entities, and log data.
The WorldContext object is an interface with the following methods:
type WorldContext interface {
// NewSearch creates a new Search object that can iterate over entities that match
// a given Component filter.
//
// For example:
// search, err := worldCtx.NewSearch(cardinal.Exact(Health{}))
// if err != nil {
// return err
// }
// err = search.Each(worldCtx, func(id cardinal.EntityID) bool {
// ...process each entity id...
// }
// if err != nil {
// return err
// }
NewSearch(filter Filter) (*Search, error)
// CurrentTick returns the current game tick of the world.
CurrentTick() uint64
// EmitEvent broadcasts an event message to all subscribed clients.
EmitEvent(event string)
// Logger returns a zerolog.Logger. Additional metadata information is often attached to
// this logger (e.g. the name of the active System).
Logger() *zerolog.Logger
getECSWorldContext() ecs.WorldContext
}
Getting Transactions
In order to get specific transactions from the queue, utilize a TransactionType
's In
method. This TransactionType
method takes the WorldContext as an argument, and returns a slice of TxData
, which contains data related to that transaction.
import "pkg.world.dev/world-engine/cardinal"
func AttackSystem(worldCtx cardinal.WorldContext) error {
txs := AttackTransaction.In(worldCtx)
for _, tx := range txs {
// do something with each tx...
msg := tx.Msg()
hash := tx.Hash()
txData := tx.Tx()
}
}
TxData
type TxData[Input any] struct {
Hash message.TxHash // unique transaction hash
Msg Msg // transaction input
Tx *sign.Transaction // Transaction data (signature, hash, message data)
}
Example System
import "pkg.world.dev/world-engine/cardinal"
// define a component
type Health struct {
Amount uint64
Cap uint64
}
func (Health) Name() string {
return "health"
}
// define an attack transaction
type AttackInput struct {
TargetPlayer uint64
Amount uint64
}
type AttackResult struct {
Success bool
}
var Attack = cardinal.NewMessageType[AttackInput, AttackResult]("attack")
// implement the system
func AttackSystem(worldCtx cardinal.WorldContext) error {
// get all attack transactions in the queue
txs := Attack.In(worldCtx)
for _, tx := range txs {
// get the underlying attack tx struct
msg := tx.Msg()
// update the component based on the attack
err := HealthComponent.Update(worldCtx, entity.ID(msg.TargetPlayer), func(health Health) Health {
health.Amount -= msg.Amount
return health
})
// if an error occurred, set the result, and add the error to the transaction.
if err != nil {
Attack.SetResult(world, tx.Hash(), AttackResult{Success: false})
Attack.AddError(world, tx.Hash(), err)
continue
}
// set result for success case
Attack.SetResult(world, tx.Hash(), AttackResult{Success: true})
}
return nil
}