Module 2 - Principles Of Analytics Events Implementation
Now that you have the analytics libraries integrated into the world it is possible to start recording in-world event data from your own scripts.
Whilst it is possible to send events directly to the Analytics Manager throughout your existing CodeBase, it is good practice to decouple the Analytics Manager from your main game logic by hooking into broadcast events. This ensures that your in-world analytics listens passively to events in your game, and does not impact the player’s experience, or require large amounts of code rewriting.
Note: Ensure you are sending analytics events from Scripts that are set to Default execution mode. You cannot send In-World Analytics data from Scripts set to Local execution mode.
Using The Analytics Manager With Existing Broadcast Events
You can use some of the existing broadcast events in the world in order to track those events using in-world analytics which keeps the number of code changes required to a minimum.
Recording loot and bounty pickups offer a good opportunity to explore this method in Chop ‘N Pop Graveyard Bash.
When the player collects ammo for their gun, or a potion to refill their health, we can record this information as a reward. In LootItem.ts there is already a broadcast event (ln 108):
this.sendNetworkBroadcastEvent(LootPickup, {
player: player,
loot: this.props.itemType,
});
The In-World Analytics reward event requires the following values to be passed:
- rewardsType (string): The type of reward earned
- rewardsEarned (number): The amount of that reward
There is a mismatch between the values that are sent with the broadcast event and those needed to fulfill the requirements of the In-World Analytics reward event - it is missing the amount of rewards!
In the Chop ‘N Pop Graveyard Bash example world the amount of ammo or health given by a reward is stored as a configurable Prop in PlayerManager.ts:
ammoPerBox: { type: PropTypes.Number, default: 10 }, healthPerPotion: { type: PropTypes.Number, default: 1 },
Rather than storing these as props in this class, and attempting to move the through the code by creating accessors or passing them as parameters in function calls, it would be more beneficial to store these in the GameConstants.ts file we created in Moule 1 so that they can be accessed from both the game logic code and the analytics code independently, and tweaked easily in the future.
- Change the GameConstants.ts so that there is storage for these values:
export const GameConstants = {
AMMO_PER_BOX: 10,
HEALTH_PER_POTION: 1,
};
- Replace the values with your settings on the PlayerManager in the World if they are different from the default values.
- Replace the handleLootPickup function in PlayerManager with the following:
private handleLootPickup(data: {player: Player, loot : string}) {
var playerData = this.gamePlayers.get(data.player);
if (playerData) {
if(data.loot === 'Ammo') {
this.gamePlayers.addAmmo(data.player, GameConstants.AMMO_PER_BOX);
} else if(data.loot === 'Potion') {
this.gamePlayers.heal(data.player, GameConstants.HEALTH_PER_POTION, this.props.playerMaxHp);
}
this.updatePlayerHUD(playerData);
}
}
- Remove the ammoPerBox and healthPerPotion props from the PlayerManager class.
- Test your world, pickup ammo and a potion to ensure that the world still works as expected.
Now that the values are available in a single, accessible format, we can implement a listener for the reward event in the AnalyticsManager.
- Open TurboAnalytics.ts and navigate to the subscribeToEvents() function.
- Replace the subscribeToEvents function, and add an onLootPickup function so that it reads as follows:
subscribeToEvents() {
this.connectLocalBroadcastEvent(TurboDebug.events.onDebugTurboPlayerEvent, (data: {
player: hz.Player;
eventData: EventData;
action: Action
}) => {
this.onDebugTurboPlayerEvent(data.player, data.eventData, data.action);
});
this.connectNetworkBroadcastEvent(Events.lootPickup, (data) => {
this.onLootPickup(data);
});
}
onLootPickup(data: { player: hz.Player, loot: string}){
let rewardsEarned = 1;
if (data.loot === 'Ammo') {
rewardsEarned = GameConstants.AMMO_PER_BOX;
} else if(data.loot === 'Potion') {
rewardsEarned = GameConstants.HEALTH_PER_POTION;
}
this.sendRewardsEarned({player: data.player, rewardsType: data.loot, rewardsEarned});
}
Now when a player collects ammo or health in the world, the Events.lootPickup event is fired, and this function is called, which uses the rewardsEarned analytics API to record the event to our In-World Analytics.
Test the world with the Console open and ensure that the event is being recorded.
Using The Analytics Manager With New Broadcast Events
Sometimes there may not be an existing broadcast event that is suitable to listen for in the Analytics Manager. In these scenarios it may be beneficial to add a broadcast event that you can then listen for in your In-World Analytics (it may also be a useful addition for other game or world logic in the future!).
Recording weapon usage offers a good example to explore for this method in Chop ‘N Pop Graveyard Bash.
- Open Events.ts in your code editor .
- Add a new event on line 13 called
weaponEquipped
, which takes the following parameters:
export const Events = {
gameReset: new NetworkEvent<{}>('gameReset'),
// Weapon Events
weaponEquipped: new NetworkEvent<{ player: Player, weaponKey: string, weaponType: string, isRightHand: boolean }>('weaponEquipped'),
// Gun Events
projectileHit: new NetworkEvent<{ hitPos: Vec3, hitNormal: Vec3, fromPlayer: Player, weapon: string}>('projectileHit'),
...
- Open Axe.ts and modify the
OnGrabStart()
function by sending the weaponEquipped
event at the end of the function:
protected override OnGrabStart(isRightHand: boolean, player: Player) {
this.entity.owner.set(player);
if (isRightHand) {
this.hand = HapticHand.Right;
} else {
this.hand = HapticHand.Left;
}
if (player.deviceType.get() != PlayerDeviceType.VR) {
this.triggerSub = this.connectCodeBlockEvent(this.entity, CodeBlockEvents.OnIndexTriggerDown, (triggerPlayer) => {
Throttler.try("AxeHit", () => {
this.entity.owner.get().playAvatarGripPoseAnimationByName(AvatarGripPoseAnimationNames.Fire);
this.sendNetworkBroadcastEvent(Events.monstersInRange, {entity: this.entity, range: this.props.threatRange})
}, this.props.swingCooldown)
});
this.inRangeSub = this.connectNetworkEvent(this.entity, Events.monstersInRangeResponse, this.hitMonsters.bind(this));
}
// Send weaponEquipped event from the axe
this.sendNetworkBroadcastEvent(Events.weaponEquipped, {player, weaponKey: "axe", weaponType: "melee", isRightHand});
}
Open GunCore.ts and modify the OnGrabStart()
function by sending the weaponEquipped
event:
OnGrabStart(isRight: boolean, player: Player) { this.reload();
this.isHeld = true;
this.entity.owner.set(player);
this.props.projectileLauncher?.owner.set(player);
this.triggerDownSubscription = this.connectCodeBlockEvent(
this.entity,
CodeBlockEvents.OnIndexTriggerDown,
this.triggerDown.bind(this)
);
this.triggerReleasedSubscription = this.connectCodeBlockEvent(
this.entity,
CodeBlockEvents.OnIndexTriggerUp,
this.triggerReleased.bind(this)
);
this.reloadSubscription = this.connectCodeBlockEvent(
this.entity,
CodeBlockEvents.OnButton1Down,
this.reload.bind(this)
);
if (player.deviceType.get() === 'VR') {
this.reloadSubscription = this.connectCodeBlockEvent(
this.entity,
CodeBlockEvents.OnButton2Down,
this.reload.bind(this)
);
}
this.playerHand = isRight ? HapticHand.Right : HapticHand.Left;
HapticFeedback.playPattern(player, HapticType.pickup, this.playerHand, this);
this.props.grabSFX?.as(AudioGizmo)?.play();
this.updateAmmoDisplay();
// Send weaponEquipped event from the gun
this.sendNetworkBroadcastEvent(Events.weaponEquipped, {player, weaponKey: "gun", weaponType: "projectile", isRightHand: isRight});
}
- Open TurboAnalytics.ts and modify the
subscribeToEvents()
function so that it reads as follows:
subscribeToEvents() {
this.connectLocalBroadcastEvent(TurboDebug.events.onDebugTurboPlayerEvent, (data: {
player: hz.Player;
eventData: EventData;
action: Action
}) => {
this.onDebugTurboPlayerEvent(data.player, data.eventData, data.action);
});
this.connectNetworkBroadcastEvent(Events.lootPickup, (data) => {
this.onLootPickup(data);
});
// Subscribe to the weaponEquipped event
this.connectNetworkBroadcastEvent(Events.weaponEquipped, (data) => {
this.sendWeaponEquip(data);
});
}
In this example, we are not adding a new function. Instead, our new event contains all the information that is required for the call to the analytics API.
Using The Analytics API Directly
Although using the pre-supplied Analytics Manager is intended to accelerate your workflow, you may find there are situations where you would prefer to work directly with the Analytics API. In this case, you can use the AnalyticsManager as an example of how to do that.
For example, if we wanted to send the weapon equipped call directly to the Analytics API we could use the following function call:
Turbo.send(TurboEvents.OnWeaponEquip, data);
This will then record the data by calling the API directly. Note that you can view required parameters for each analytics event in the Horizon Analytics TypeScript definitions file (horizon_analytics.d.ts) or in the
TurboEvents API reference.
OnWeaponEquip: SinglePlayerTurboEvent<{
weaponKey: string;
weaponType?: string | undefined;
isRightHand?: boolean | undefined;
}>;