Advanced features
Event Subscription Details
In the previous sections, we learned how to initialize the world state and interact with contract methods. Now, we need to add a subscription function that listens to data based on schema names and event names. This enables real-time world state updates during gameplay, effectively decoupling state modification (contract method calls) from state updates.
/**
* Handles real-time game events through WebSocket subscription
* @param dubhe - Dubhe client instance
*/
const subscribeToEvents = async (dubhe: Dubhe) => {
try {
// Get all players for reference
const allPlayers = await dubhe.getStorage({
name: "player",
});
// Subscribe to multiple event types
const sub = await dubhe.subscribe(
["position", "encounter", "monster_catch_attempt_event", "player"],
(data) => {
console.log("Received real-time data:", data);
// Handle player position updates
if (data.name === "position") {
const position = data.value;
const playerAddress = data.key1;
// Update hero position
setHero((prev) => ({
...prev,
position: {
left: position.x * STEP_LENGTH,
top: position.y * STEP_LENGTH,
},
}));
// Update other players' positions
if (allPlayers.data.find((p) => p.key1 === playerAddress)) {
setPlayers((prev) => {
const newPlayers = [...prev];
const playerIndex = newPlayers.findIndex(
(p) => p.address === playerAddress
);
if (playerIndex > -1) {
newPlayers[playerIndex].position = {
left: position.x * STEP_LENGTH,
top: position.y * STEP_LENGTH,
};
} else {
newPlayers.push({
address: playerAddress,
position: {
left: position.x * STEP_LENGTH,
top: position.y * STEP_LENGTH,
},
});
}
return newPlayers;
});
}
}
// Handle monster encounter updates
else if (data.name === "encounter") {
const shouldLock = !!data.value;
setMonster({ exist: shouldLock });
setHero((prev) => ({ ...prev, lock: shouldLock }));
if (shouldLock) {
setSendTxLog({
display: true,
content: "Have monster",
yesContent: "Throw",
noContent: "Run",
});
} else if (data.value === null) {
setSendTxLog((prev) => ({ ...prev, display: false }));
}
}
// Handle monster catch attempt results
else if (data.name === "monster_catch_attempt_event") {
const result = Object.keys(data.value.result)[0];
toast("Monster catch attempt event received", {
description: `Result: ${CATCH_RESULTS[result]}`,
});
if (!data.value.result.Missed) {
setSendTxLog((prev) => ({ ...prev, display: false }));
setMonster({ exist: false });
setHero((prev) => ({ ...prev, lock: false }));
}
}
}
);
setSubscription(sub);
} catch (error) {
console.error("Failed to subscribe to events:", error);
}
};
Explanation
The subscription system allows us to:
- Monitor real-time changes in player positions
- Track monster encounters and catch attempts
- Update the game state automatically when other players join
- Maintain a synchronized multiplayer experience
The dubhe.subscribe()
method accepts:
- An array of event names to monitor
- A callback function that handles incoming events
- Returns a subscription object for cleanup
For more detailed information about the indexer system and its capabilities, please refer to the Indexer Documentation.
Position Event Handling
if (data.name === "position") {
const position = data.value;
const playerAddress = data.key1;
// Update hero position
setHero((prev) => ({
...prev,
position: {
left: position.x * STEP_LENGTH,
top: position.y * STEP_LENGTH,
},
}));
}
- Converts blockchain coordinates to screen coordinates
- Updates both the current player and other players’ positions
Monster Info Event Handling
else if (data.name === "encounter") {
const shouldLock = !!data.value;
setMonster({ exist: shouldLock });
setHero((prev) => ({ ...prev, lock: shouldLock }));
}
- Controls player movement during monster encounters
- Updates UI to show monster presence
- Manages interaction options (Throw/Run)
Monster Catch Attempt Event Handling
else if (data.name === "monster_catch_attempt_event") {
const result = Object.keys(data.value.result)[0];
// ... result handling
}
- Processes catch attempt outcomes
- Updates game state based on success/failure
- Shows feedback via toast notifications
State Management
- Uses React state hooks for UI updates
- Maintains WebSocket connection reference
- Handles cleanup on component unmount
Performance Considerations
- Optimizes updates by checking existing players
- Uses efficient state updates to prevent unnecessary renders
- Properly manages WebSocket connection lifecycle
Make it multiplayer
You may not have realized it, but you’ve just made a game that is almost completely ready to become massively multiplayer. Dubhe has handled all of the network code out-of-the-box and there is a naturally shared, accessible database via the blockchain.
And you only need to implement this logic with a simple piece of code.
export function Map({
width,
height,
terrain,
players,
type,
ele_description,
events,
map_type,
metadata,
}: Props) {
return (
<>
{players?.map(
(player) =>
player.address !== hero.name && (
<div
key={player.address}
id="moving-block"
style={{
left: `${player.position.left}vw`,
top: `${player.position.top}vw`,
}}
>
<div id="hero-name">{`${player.address.slice(0, 6)}`}</div>
<div className="xiaozhi">
<img src="/assets/player/S.gif" alt="" />
</div>
</div>
)
)}
</>
);
}