Observers and Entities
Often, you'll want to run code over the lifetime of an entity. You can use observe
for all kinds of lists and records of entities, from players to in-game matches.
- 📚 What entities and observers are
- 🔍 How to track an entity manually
- 🔗 How to use
observe
to track entities - 1️⃣ How to use
observeWhile
to track one value
The Observer pattern
In Reflex, an entity is a record of data that represents a player, match, or other object in your game. You can use entities to store data about the object, like its health or position.
An Observer is a function that binds some logic to an entity over its lifetime. The lifetime of an entity is the time between when it's created and when it's removed from the record.
An Observer can be used to:
- Spawn players when they're added to an ongoing game
- Apply temporary status effects to players
- Clean up connections when players are eliminated or disconnect
And much more! Read more about the Observer pattern →
Understanding Observers
On Subscribing to State, you learned how to use subscribe
, once
, and wait
to run side effects for a player's health bar. But the examples only covered running side effects on one player. How can you bind this extra logic for the lifetime of every player?
We can use observe
to do this, but to understand how to use observe
, let's first look at how you might write your own Observer.
A general implementation of Observers requires a few things:
- A record of entities to track
- Call the Observer when an entity is added, and clean up when it's removed
- A way to identify the entity to track its whole lifetime
Selecting entities
To track entities, you need a record of them. In the Player list example, the players
slice stores a map of players by their ID. You can write a selector that returns this record:
- TypeScript
- Luau
import { RootState } from "./producer";
export const selectPlayersById = (state: RootState) => {
return state.players.entities;
};
local producer = require(script.Parent.producer)
local function selectPlayersById(state: producer.RootState)
return state.players.entities
end
Tracking additions
To track when an entity is added to the record, you can use the current and previous states passed to subscribe
. When a player is added to the record, the previous state will not have the player, but the current state will. You can use this to filter out new players:
- TypeScript
- Luau
function entityAdded(entity: PlayerEntity) {
// Player was added
}
producer.subscribe(selectPlayersById, (current, previous) => {
for (const [id, player] of pairs(current)) {
if (previous[id] === undefined) {
entityAdded(player);
}
}
});
local function entityAdded(entity: players.PlayerEntity)
-- Player was added
end
producer:subscribe(selectPlayersByid, function(current, previous)
for id, player in current do
if previous[id] == nil then
entityAdded(player)
end
end
end)
Waiting for deletion
To track when an entity is removed from the record, you can use once
to create a listener that runs when the entity is not in the new state:
- TypeScript
- Luau
function entityAdded(entity: PlayerEntity) {
const doesNotHaveEntity = (entities: PlayerEntityRecord) => {
return entities[entity.id] === undefined;
};
producer.once(selectPlayersById, doesNotHaveEntity, () => {
// Player was removed
});
}
local function entityAdded(entity: players.PlayerEntity)
local function doesNotHaveEntity(entities: players.PlayerEntityRecord)
return entities[entity.id] == nil
end
producer:once(selectPlayersById, doesNotHaveEntity, function()
-- Player was removed
end)
end
Calling the Observer
Now that you have a way to track entities, you can connect an Observer to the entity's lifetime:
- TypeScript
- Luau
function playerObserver(player: PlayerEntity) {
// Player was added
return () => {
// Player was removed
};
}
function entityAdded(entity: PlayerEntity) {
const doesNotHaveEntity = (entities: PlayerEntityRecord) => {
return entities[entity.id] === undefined;
};
const cleanup = playerObserver(entity);
producer.once(selectPlayersById, doesNotHaveEntity, () => {
cleanup();
});
}
producer.subscribe(selectPlayersById, (current, previous) => {
for (const [id, player] of pairs(current)) {
if (previous[id] === undefined) {
entityAdded(player);
}
}
});
local function playerObserver(player: players.PlayerEntity)
-- Player was added
return function()
-- Player was removed
end
end
local function entityAdded(entity: players.PlayerEntity)
local function doesNotHaveEntity(entities: players.PlayerEntityRecord)
return entities[entity.id] == nil
end
local cleanup = playerObserver(entity)
producer:once(selectPlayersById, doesNotHaveEntity, function()
cleanup()
end)
end
producer:subscribe(selectPlayersById, function(current, previous)
for id, player in current do
if previous[id] == nil then
entityAdded(player)
end
end
end)
We've brought the Observer pattern into Reflex! You can run side effects in the playerObserver
function, and clean them up when the Observer is removed.
While this works, it's a lot of code to write for something that should be simple. This is where observe
comes in.
Observers in Reflex
The observe
method is a shorthand for creating Observers. It takes a selector, a discriminator, and an Observer function.
- The selector is used to select a record of entities to track, and it can return an array or a dictionary. We will use the
selectPlayersById
selector from earlier. - The discriminator is a function that takes an entity and its index, and returns a value that uniquely identifies the entity. Here, we will use the
id
property of the entity. - The Observer function runs when an entity is added, and returns an optional cleanup function that runs when the entity is removed.
- TypeScript
- Luau
const getPlayerId = (player: PlayerEntity, index: string) => {
return player.id;
};
producer.observe(selectPlayersById, getPlayerId, (player, index) => {
// Player was added
return () => {
// Player was removed
};
});
local function getPlayerId(player: players.PlayerEntity, index: string)
return player.id
end
producer:observe(selectPlayersById, getPlayerId, function(player, index)
-- Player was added
return function()
-- Player was removed
end
end)
This is essentially the same as our custom Observers, but with much less code! The observe
method will automatically track when the entity is added and removed, and run the Observer function accordingly.
The discriminator function is optional. If you don't provide one, the entity itself will be used as the discriminator. This is only recommended if the entity is a primitive value, like a string or number.
If your entity doesn't store a unique ID, you can identify it by the
index
argument passed to the discriminator. This is only recommended if the entities are instead mapped to an ID, and the index is stable.
Observing entities
On Subscribing to State, we left off at playing a sound when one player gets damaged. We made a selector factory to select the health of a player by ID, and subscribed to a specific player's health.
But now that we can observe the lifetime players, we can use observe
to play a sound when any player gets damaged:
- TypeScript
- Luau
const selectPlayerHealthById = (id: string) => {
return (state: RootState) => {
return state.players.entities[id].health;
};
};
const didDecrease = (current: number, previous: number) => {
return current < previous;
};
producer.observe(selectPlayersById, getPlayerId, (player, index) => {
const selectHealth = selectPlayerHealthById(player.id);
const unsubscribe = producer.subscribe(selectHealth, didDecrease, () => {
// Play sound
});
return unsubscribe;
});
local function selectPlayerHealthById(id: string)
return function(state: RootState)
return state.players.entities[id].health
end
end
local function didDecrease(current: number, previous: number)
return current < previous
end
producer:observe(selectPlayersById, getPlayerId, function(player, index)
local selectHealth = selectPlayerHealthById(player.id)
local unsubscribe = producer:subscribe(selectHealth, didDecrease, function()
-- Play sound
end)
return unsubscribe
end)
Observing a condition
Alternatively, you might only want to create an Observer while a certain condition is met, and destroy it when the condition becomes falsy. With observeWhile
, you can create an Observer that only runs while a selector or predicate returns a truthy value.
For example, your state might have a round
record that contains a status
property. To create an Observer while the round is in-progress, and destroy it when the round ends, you can use observeWhile
like so:
- TypeScript
- Luau
const selectRoundStatus = (state: RootState) => {
return state.round.status;
};
const isRoundInProgress = (status: RoundStatus) => {
return status === "in-progress";
};
producer.observeWhile(selectRoundStatus, isRoundInProgress, (status) => {
// Round is in-progress
return () => {
// Round is not in-progress
};
});
local function selectRoundStatus(state: RootState)
return state.round.status
end
local function isRoundInProgress(status: RoundStatus)
return status == "in-progress"
end
producer:observeWhile(selectRoundStatus, isRoundInProgress, function(status)
-- Round is in-progress
return function()
-- Round is not in-progress
end
end)
If your Observer doesn't need the value of your selector, you can omit the predicate
argument and just return whether the status is "in-progress"
in the selector:
- TypeScript
- Luau
const selectRoundInProgress = (state: RootState) => {
return state.round.status === "in-progress";
};
producer.observeWhile(selectRoundInProgress, () => {
// Round is in-progress
return () => {
// Round is not in-progress
};
});
local function selectRoundInProgress(state: RootState)
return state.round.status == "in-progress"
end
producer:observeWhile(selectRoundInProgress, function()
-- Round is in-progress
return function()
-- Round is not in-progress
end
end)
Summary
You're now ready to use Reflex in your games! The guides from here on out will focus on more advanced topics, but you can always refer back to the earlier guides if you need a refresher.
Let's recap what we've learned about Observers:
- Entities are unique objects that can be added and removed from the state.
- Observers are functions that run over the lifetime of some value.
- Use
observe
to create Observers for unique entities. - Use
observeWhile
to create an Observer while a condition is met.