Skip to main content

๐Ÿ“ก Lesson 8.3: Live-Ops โ€” Analytics, Remote Config & Crash Reporting

Shipping used to be the finish line. For a modern game it's the starting gun. Once players are in your build, three questions dominate: what are they actually doing? (analytics), how do I change the game without shipping a new build? (remote config), and what's breaking for them that never broke for me? (crash reporting). This lesson tours Unity Gaming Services' live-ops toolkit โ€” mostly conceptually, with one concrete Remote Config fetch โ€” and closes on the privacy responsibilities that come with collecting any of this data.

๐ŸŽฏ Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what Unity Gaming Services (UGS) offers for running a live game
  • Describe how Analytics events build into funnels that reveal where players drop off
  • Use Remote Config to change balancing and feature flags without a new build, and fetch values in code
  • Understand Cloud Diagnostics crash and exception reporting from the wild
  • Explain A/B testing and feature flags as ways to learn and de-risk changes
  • Handle player-data privacy and consent responsibly

Estimated Time: 60 minutes  ยท  Prerequisite: Lesson 8.2 (a shipped, CI-built game is what you run live-ops on); async basics (Lesson 1.3)

In This Lesson

The Live-Ops Loop

Live-ops (live operations) is the practice of running a game as an ongoing service after launch: watching how it's played, adjusting it, and repeating. It's a loop, and every tool in this lesson is one arc of it.

flowchart LR A["๐Ÿš€ Ship
the build"] --> B["๐Ÿ“Š Measure
analytics + crashes"] B --> C["๐Ÿง  Learn
where players drop / break"] C --> D["๐ŸŽ›๏ธ Configure
remote config + flags"] D --> E["๐Ÿงช Experiment
A/B test a change"] E --> B D -.->|"occasionally"| A

Figure 1: The live-ops loop. You ship once, then cycle: measure real behaviour, learn from it, configure the game remotely, and experiment to validate changes โ€” feeding the next measurement. Most iterations never touch the build; only occasionally do you ship a new binary.

The crucial insight is the dashed line: most adjustments loop back through remote configuration, not through a new build. Shipping a binary is slow โ€” store review, player download, version fragmentation. Changing a server-side value is instant and reaches everyone at once. Live-ops is largely the art of moving as much as possible out of the build and onto the server.

Unity Gaming Services

Unity Gaming Services (UGS) is Unity's umbrella of backend, cloud-hosted services you call from the game and administer from the Unity Dashboard. It spans far more than live-ops โ€” authentication, cloud save, matchmaking, lobbies, economy, hosted servers โ€” but this lesson focuses on the four live-ops pillars: Analytics, Remote Config, Cloud Diagnostics, and the experimentation built on top of them.

They share plumbing. You link the project to a UGS project ID, install the relevant packages, and initialise the core services once at startup. From then on each service is a small async API:

using Unity.Services.Core;
using Unity.Services.Analytics;
using UnityEngine;

public class ServicesBootstrap : MonoBehaviour
{
    private async void Start()
    {
        // One-time initialisation of Unity Gaming Services.
        await UnityServices.InitializeAsync();

        // Only start collecting analytics after the player has consented.
        if (PlayerConsent.AnalyticsGranted)
            AnalyticsService.Instance.StartDataCollection();
    }
}

That await UnityServices.InitializeAsync() uses exactly the async/await you learned in Lesson 1.3 โ€” UGS is thoroughly asynchronous because every call is a network round-trip. Note the consent gate on the very first line of real work; we'll return to why it must be there.

Analytics โ€” Events & Funnels

Analytics answers "what are players actually doing?" by recording events โ€” timestamped records of things that happened, each with a name and optional parameters. Some are standard (session start, level complete); most valuable are the custom events you define for your game's own moments:

using Unity.Services.Analytics;

// Record a custom event when a player finishes a level.
void OnLevelComplete(int level, float seconds, int deaths)
{
    var evt = new CustomEvent("levelComplete")
    {
        { "levelIndex", level },
        { "durationSeconds", seconds },
        { "deaths", deaths }
    };
    AnalyticsService.Instance.RecordEvent(evt);
}

One event is a data point; the power comes from aggregating thousands of them. The classic aggregation is a funnel: an ordered sequence of steps, showing how many players reach each one. If tutorialStart โ†’ tutorialStep2 โ†’ tutorialComplete โ†’ firstLevelStart shows 100% โ†’ 92% โ†’ 61% โ†’ 58%, you've just discovered that a third of players abandon on tutorial step 2 โ€” a precise, actionable finding you'd never get from watching a handful of playtesters.

๐Ÿ“– Definition

A funnel is an analytics view of an ordered set of events that measures drop-off at each step โ€” the percentage of players who reach step N but not N+1. Steep drops flag friction: a confusing tutorial beat, a difficulty spike, a paywall players won't cross. Funnels turn "players seem to quit early" into "62% quit at the second boss."

Design your events deliberately: name them consistently, keep parameter types stable (changing a parameter from int to string mid-flight fragments your data), and record the moments that map to real decisions. Analytics you can't act on is just storage cost.

Remote Config

Remote Config is the counterpart to analytics: where analytics reads player behaviour, Remote Config writes game parameters โ€” from the server, live, without a new build. You define keys in the Unity Dashboard (an enemy's health, a drop rate, a feature on/off flag, an event's start date) and the game fetches their current values at runtime. Change the value in the dashboard and every player picks it up on their next fetch.

The code pattern is: fetch once (typically at startup or level load), then read typed values from the returned store.

using Unity.Services.Core;
using Unity.Services.RemoteConfig;
using UnityEngine;

public struct UserAttributes { }    // targeting inputs (empty here)
public struct AppAttributes { }

public class Balancing : MonoBehaviour
{
    public float BossHealth { get; private set; }
    public bool WeekendEventActive { get; private set; }

    private async void Start()
    {
        await UnityServices.InitializeAsync();

        RemoteConfigService.Instance.FetchCompleted += OnFetched;
        await RemoteConfigService.Instance.FetchConfigsAsync(
            new UserAttributes(), new AppAttributes());
    }

    private void OnFetched(ConfigResponse response)
    {
        var cfg = RemoteConfigService.Instance.appConfig;

        // Read typed values, always with a sensible default fallback
        // in case the fetch failed or the key is missing.
        BossHealth        = cfg.GetFloat("boss_health", 500f);
        WeekendEventActive = cfg.GetBool("weekend_event", false);

        Debug.Log($"Boss health from server: {BossHealth}");
    }
}

Two habits make this safe. First, always pass a default to every getter โ€” the fetch can fail (no network) or a key can be absent, and the game must still play. Second, apply values at sensible boundaries, not mid-combat, so a config change doesn't yank a boss's health bar while a player is fighting it.

โœ… Why this is transformative

The classic use is balancing: a weapon is overpowered, you nerf it by editing one dashboard value, and the fix reaches every player in minutes โ€” no build, no store review, no download. Remote Config is what turns "we'll fix it in the next patch" (weeks) into "fixed" (minutes).

A/B Testing & Feature Flags

Remote Config plus Analytics unlocks two powerful practices that separate guessing from knowing.

๐Ÿงช A/B testing

Serve different config values to different randomly-assigned player segments โ€” group A gets a 3-life tutorial, group B gets 5 โ€” then compare a funnel or retention metric between them. You measure which variant works instead of arguing about it. The winner becomes the default; the loser is retired.

๐Ÿšฉ Feature flags

A boolean Remote Config key that gates a feature. Ship the code dark (flag off), then flip it on for staff, then 5% of players, then everyone โ€” and flip it off instantly if it misbehaves. Decouples deploying code from releasing a feature.

Both rest on the same idea: the build contains the capability, the server decides who gets it and how. That's the payoff of pushing decisions out of the binary โ€” a risky feature can launch to a sliver of your audience and be pulled without an emergency patch. A/B testing then closes the loop from Figure 1's "experiment" arc, feeding a real measurement back into the next decision.

๐Ÿ’ก Kill switches are feature flags too. Wrapping a new, risky subsystem (a live event, a matchmaking change) in a flag gives you an instant "off" button when something goes wrong in production โ€” often the difference between a shrug and an outage.

Crash & Exception Reporting

Your game passed every test in Lesson 8.1 and CI shipped it green โ€” and it will still crash for some players, on hardware and OS versions and edge cases you never saw. Cloud Diagnostics (Unity's crash and exception reporting service) captures those failures from the wild and aggregates them in the dashboard, so you learn about them from data rather than from angry reviews.

Once enabled, it automatically reports uncaught exceptions and native crashes, grouping identical stack traces so a bug hitting 10,000 players shows as one issue with a count โ€” not 10,000 noise entries. Each report carries the stack trace, device model, OS, and app version (this is where the versioning from Lesson 8.2 pays off: a report pins to an exact build). You can also log caught issues deliberately:

try
{
    LoadPlayerSave();
}
catch (System.Exception e)
{
    // Report a handled exception with context, without crashing the game.
    UnityEngine.Diagnostics.Utils.ForceCrash(
        UnityEngine.Diagnostics.ForcedCrashCategory.Abort); // (illustrative)
    Debug.LogException(e);   // logged exceptions are captured by Cloud Diagnostics
}

In practice you rarely force anything โ€” you let unhandled exceptions flow to the reporter and use Debug.LogException for handled ones you still want visibility on. The workflow that matters: sort by frequency, fix the crash hurting the most players first, ship the fix through your CI pipeline, and watch the issue's count fall in the dashboard. That's the measure-fix-verify loop applied to stability.

โš ๏ธ A crash report without a symbolicated build is half a clue

Release builds (especially IL2CPP, which you'll meet in Lesson 8.4) strip symbols, so raw crash stacks are addresses, not method names. Upload your build's symbol files to the diagnostics service as part of the pipeline so reports resolve to readable stack traces. Wire this into CI once and every future crash is legible.

Privacy & Consent

Everything in this lesson collects data about real people, and that carries legal and ethical obligations you cannot treat as an afterthought. Regulations such as the GDPR (Europe) and CCPA (California), plus the app stores' own rules, govern what you may collect, how you disclose it, and the rights players have over it.

The practical requirements for a shipping game:

  • Consent before collection. Ask the player's permission before starting analytics data collection โ€” which is why the bootstrap earlier gated StartDataCollection() behind a consent check rather than calling it unconditionally.
  • Disclosure. A clear privacy policy stating what you collect and why, linked from the game and the store listing.
  • Honour data-subject rights. Support opt-out and data-deletion requests; UGS Analytics exposes APIs (e.g. a data-deletion/opt-out request) to help you comply.
  • Extra care for minors. Games likely to be played by children carry stricter rules (COPPA and similar); when in doubt, collect less.

โš ๏ธ "Collect everything, decide later" is not a strategy

Beyond the legal risk, over-collection erodes player trust and bloats your data. Collect the minimum that answers a real question, be transparent about it, and make opting out easy. Respecting players' data is part of being a professional โ€” and this course's last technical topic is deliberately one about responsibility, not just capability.

Hands-on Challenge

๐Ÿ‹๏ธ Exercise 1: Design a tutorial funnel

Objective: Translate a design worry into measurable events.

Your team suspects players quit during the tutorial but doesn't know where. List the custom analytics events you'd record to build a funnel that pinpoints the drop-off, and describe how you'd read the result.

โœ… A reasonable answer

Record an event at each tutorial beat: tutorialStart, tutorialMoveTaught, tutorialJumpTaught, tutorialCombatTaught, tutorialComplete, then firstLevelStart. Configure a funnel over them in that order. The step with the steepest percentage drop is where players abandon โ€” say tutorialCombatTaught retains 90% but tutorialComplete only 55%, pointing at the combat lesson as the friction point. You'd then A/B test a simplified combat tutorial (feature-flagged via Remote Config) against the current one and compare completion rates.

๐Ÿ‹๏ธ Exercise 2: Balance without a build

Two days after launch, one weapon is dominating and players are frustrated. Describe, using this lesson's tools, how you'd address it within an hour and without shipping an update โ€” and what safeguard keeps the fix from breaking offline players.

โœ… Approach

Expose the weapon's damage as a Remote Config key (e.g. plasma_rifle_damage) that the game already reads via GetFloat("plasma_rifle_damage", 45f). Lower the value in the Unity Dashboard; players pick up the nerf on their next config fetch, no build required. The safeguard is the default passed to GetFloat: if a player has no network and the fetch fails, they fall back to the sane baked-in value and the game still works. Optionally announce the change and watch an analytics metric (win-rate with that weapon) to confirm the fix landed.

๐ŸŽฏ Quick Quiz

Question 1: What is a funnel in analytics?

Question 2: The chief advantage of Remote Config is that it lets youโ€ฆ

Question 3: Why pass a default to every cfg.GetFloat("key", default) call?

Question 4: When should a game call AnalyticsService.Instance.StartDataCollection()?

Summary

๐ŸŽ‰ Key Takeaways

  • Live-ops is a loop: ship once, then measure โ†’ learn โ†’ configure โ†’ experiment, mostly without new builds.
  • Unity Gaming Services provides the cloud backends; you initialise once with await UnityServices.InitializeAsync() and call each as a small async API.
  • Analytics records custom events that aggregate into funnels, pinpointing where players drop off.
  • Remote Config changes balancing and feature flags server-side; always read values with a default fallback and apply them at safe boundaries.
  • A/B testing measures which variant wins; feature flags decouple deploying code from releasing a feature and give you a kill switch.
  • Cloud Diagnostics captures real crashes/exceptions, grouped and tied to a build version; symbolicate release builds so stacks are legible.
  • All of this collects player data โ€” get consent, disclose, honour opt-out/deletion, and collect the minimum.

๐Ÿš€ What's Next?

You now have every piece of the professional pipeline โ€” tests, CI, and live-ops. In the finale, Lesson 8.4: A Tested, CI-Built Release โ€” and Where to Go Next, you'll combine them into one build-along release: write tests and a build script, wire a CI workflow, and walk a real release checklist from version bump to smoke test. Then we'll look back over the whole Fundamentals โ†’ Intermediate โ†’ Advanced journey and point the way to everything still ahead.

๐Ÿ“ก The game talks back now

A shipped build is no longer a black box. You can see what players do, change the game beneath their feet, and catch what breaks โ€” responsibly. That's what running a live game means.