> ## Documentation Index
> Fetch the complete documentation index at: https://docs-unity.molca.id/llms.txt
> Use this file to discover all available pages before exploring further.

# Add scoring to VR activities

> Configure scoring for VR training activities with step-level scoring and session integration.

**Applies to:** Molca VR SDK

## Overview

This recipe shows you how to add scoring to VR training activities. You'll configure step-level scoring with `StepScoringAuxiliary`, aggregate scores at the activity level with `ActivityScoring`, and choose appropriate scoring types for different training objectives. This enables performance tracking, session posting, and real-time score feedback during VR training scenarios.

## Prerequisites

* **SDK modules**: Molca Core, Molca VR SDK installed
* **Unity setup**: RuntimeManager configured, VR scenario set up with ScenarioManager and activities
* **Prior knowledge**: [SequenceController](/core/sequence-controller), [Step](/core/step), [ScenarioActivity](/vr/scenario-activity)
* **Recommended**: Complete [Recipe: Set up a VR scenario](/recipes/setup-vr-scenario) first

## Step-by-step

### Step 1: Add StepScoringAuxiliary to steps

Add `StepScoringAuxiliary` components to individual steps that should contribute to the activity score.

```csharp theme={null}
// In the Unity Editor:
// 1. Select a Step GameObject in your activity hierarchy
// 2. In the Step component Inspector, expand "Auxiliaries"
// 3. Increase the Auxiliaries array size by 1
// 4. In the new element, select "Add Component" → Molca → VR → Scoring → Step Scoring Auxiliary
// 5. The StepScoringAuxiliary component appears in the Auxiliaries list
```

**Why this works**: `StepScoringAuxiliary` is a step auxiliary that hooks into the step lifecycle (`OnStepBegin`, `OnStepUpdate`, `OnStepCompleted`). It automatically tracks elapsed time, calculates scores based on the configured `ScoringConfig`, and optionally posts results to the session backend.

### Step 2: Configure step scoring type

Choose a `ScoringType` that matches your training objective for each step.

```csharp theme={null}
// In the Unity Editor:
// 1. Select the StepScoringAuxiliary in the Step's Auxiliaries list
// 2. Expand "Scoring Config"
// 3. Set "Scoring Type" based on your objective:
//
// For fixed-point steps (completion = points):
//    - Type: PointValue
//    - Base Points: 100
//
// For correct/incorrect validation:
//    - Type: Binary
//    - Correct Points: 100
//    - Incorrect Points: 0
//
// For speed-based scoring (faster = more points):
//    - Type: TimeBonus
//    - Target Time Seconds: 10 (ideal completion time)
//    - Max Time Seconds: 30 (slowest acceptable time)
//    - Max Points: 100 (score at target time)
//    - Min Points: 20 (score at max time)
//
// For time penalty (slower = fewer points):
//    - Type: TimePenalty
//    - Max Points: 100 (starting score)
//    - Points Per Second: 2 (deduction rate)
//    - Min Points: 0 (floor)
```

**Why this works**: Each `ScoringType` implements a different scoring formula in `ScoringConfig.CalculateScore()`. Time-based types (`TimeBonus`, `TimePenalty`, `Countdown`) refresh every frame during `OnStepUpdate`, while static types (`PointValue`, `Binary`) calculate once on completion. The `scoreMultiplier` field scales all results, and `allowNegativeScore` controls clamping.

### Step 3: Set accuracy for Binary and Accuracy scoring

For steps that validate correctness, call `SetCorrect()`, `SetIncorrect()`, or `SetAccuracy()` before the step completes.

```csharp theme={null}
using Molca.Sequence;
using MolcaSDK.VR.Scenario.Scoring;
using UnityEngine;

/// <summary>
/// Custom step that validates user action and sets scoring accuracy.
/// </summary>
public class ValidatedActionStep : Step
{
    [SerializeField] private bool requireCorrectTool = true;
    
    private StepScoringAuxiliary _scoring;

    protected override void OnStepActivated()
    {
        base.OnStepActivated();
        
        // Get the scoring auxiliary from this step's auxiliaries list
        _scoring = GetAuxiliary<StepScoringAuxiliary>();
    }

    public void OnUserAction(bool usedCorrectTool)
    {
        if (_scoring != null)
        {
            if (usedCorrectTool)
            {
                _scoring.SetCorrect(); // Sets accuracy to 1.0
            }
            else
            {
                _scoring.SetIncorrect(); // Sets accuracy to 0.0
            }
        }
        
        // Complete the step (scoring finalizes automatically)
        Complete();
    }

    public void OnPartialSuccess(float accuracyPercent)
    {
        // For Accuracy scoring type, set partial credit (0-100)
        _scoring?.SetAccuracyPercent(accuracyPercent);
        Complete();
    }
}
```

**Why this works**: `StepScoringAuxiliary` defaults to `accuracyValue = 1.0` (full credit). For `Binary` scoring, accuracy >= 0.5 awards `correctPoints`, otherwise `incorrectPoints`. For `Accuracy` scoring, the value lerps between `minPoints` and `maxPoints`. Calling these methods before `Complete()` ensures the accuracy is set before `FinalizeScore()` runs.

### Step 4: Add ActivityScoring component

Add an `ActivityScoring` component to the activity root GameObject to aggregate step scores.

```csharp theme={null}
// In the Unity Editor:
// 1. Select the ScenarioActivity GameObject (the one with ScenarioActivity component)
// 2. Add Component → Molca → VR → Scoring → Activity Scoring
// 3. Configure ActivityScoring:
//    - Include Step Scores: Check this to aggregate step scores
//    - Aggregation Mode: Sum (adds all step scores together)
//      Other options: Average, Maximum, Minimum
//    - Activity Scoring Config:
//      - Scoring Type: None (if only aggregating step scores)
//      - Or configure a base activity score (e.g., PointValue for completion bonus)
```

**Why this works**: `ActivityScoring` is marked `[RequireComponent(typeof(ScenarioActivity))]` and automatically collects all `StepScoringAuxiliary` components from the activity's `SequenceController.Steps` on `OnActivityStarted`. It combines them using the selected `ScoreAggregationMode` and adds any base activity score from `activityScoringConfig`.

### Step 5: Configure activity-level scoring (optional)

Add a base activity score that applies regardless of step performance.

```csharp theme={null}
// In the Unity Editor:
// 1. Select the ActivityScoring component
// 2. Expand "Activity Scoring Config"
// 3. Configure a base score:
//
// For activity completion bonus:
//    - Scoring Type: PointValue
//    - Base Points: 50 (bonus for completing the activity)
//
// For activity time bonus:
//    - Scoring Type: TimeBonus
//    - Target Time Seconds: 60 (ideal activity completion time)
//    - Max Time Seconds: 180 (maximum acceptable time)
//    - Max Points: 100
//    - Min Points: 20
//
// 4. This score is added to the aggregated step scores
```

**Why this works**: The activity's own `ScoringConfig` can contribute a base score independent of step scores. This is useful for rewarding overall activity completion speed or providing a fixed bonus. The activity score updates during `Update()` if the activity is active and the config is time-based.

### Step 6: Subscribe to score events for UI updates

Listen to `OnScoreChanged` and `OnScoreFinalized` events to update UI displays.

```csharp theme={null}
using MolcaSDK.VR.Scenario.Scoring;
using UnityEngine;
using TMPro;

/// <summary>
/// Displays real-time activity score in VR UI.
/// </summary>
public class ActivityScoreDisplay : MonoBehaviour
{
    [SerializeField] private ActivityScoring activityScoring;
    [SerializeField] private TextMeshProUGUI scoreText;
    [SerializeField] private TextMeshProUGUI percentageText;

    private void OnEnable()
    {
        if (activityScoring != null)
        {
            activityScoring.OnScoreChanged += UpdateScore;
            activityScoring.OnScoreFinalized += FinalizeScore;
        }
    }

    private void OnDisable()
    {
        if (activityScoring != null)
        {
            activityScoring.OnScoreChanged -= UpdateScore;
            activityScoring.OnScoreFinalized -= FinalizeScore;
        }
    }

    private void UpdateScore(float currentScore)
    {
        scoreText.text = $"Score: {currentScore:F0}";
        
        float maxPossible = activityScoring.GetMaxPossibleScore();
        float percentage = activityScoring.GetScorePercentage();
        percentageText.text = $"{percentage:F1}%";
    }

    private void FinalizeScore(float finalScore)
    {
        scoreText.text = $"Final Score: {finalScore:F0}";
        scoreText.color = Color.green; // Visual feedback for completion
    }
}
```

**Why this works**: `OnScoreChanged` fires whenever any step score updates (including time-based scores that refresh every frame). `OnScoreFinalized` fires once when the activity completes. `GetMaxPossibleScore()` returns the theoretical maximum based on all step and activity scoring configs, useful for percentage calculations.

### Step 7: Enable session posting (optional)

Configure step scoring to post results to the backend session system.

```csharp theme={null}
// In the Unity Editor:
// 1. Select a StepScoringAuxiliary component
// 2. Check "Post Score To Session"
// 3. Ensure your scenario has:
//    - ScenarioDataConfig with OrgScenarioId set
//    - ActivityData with org_activity_id set
//    - Step data with org_step_id set
//    - Active ScenarioSessionManager
//
// When the step completes, PostStepScoreAsync() runs automatically
```

**Why this works**: `StepScoringAuxiliary.PostStepScoreAsync()` runs after `FinalizeScore()` if `postScoreToSession` is true. It resolves the `ScenarioSessionManager`, `ScenarioActivity`, and org IDs from the scenario data config, then posts the score, elapsed time, and accuracy to the backend. This enables persistent progress tracking and analytics.

## Complete example

Here's a complete fire extinguisher training activity with multi-step scoring:

```csharp theme={null}
// Activity structure in Unity Hierarchy:
// ScenarioActivity_FireExtinguisher (ScenarioActivity + ActivityScoring)
//   ├─ Step1_LookAtExtinguisher (LookAtStep + StepScoringAuxiliary)
//   ├─ Step2_GrabExtinguisher (GrabStep + StepScoringAuxiliary)
//   ├─ Step3_AimAtFire (AimStep + StepScoringAuxiliary)
//   └─ Step4_ExtinguishFire (ExtinguishStep + StepScoringAuxiliary)

// Step1_LookAtExtinguisher scoring config:
// - Type: PointValue
// - Base Points: 10 (simple completion)

// Step2_GrabExtinguisher scoring config:
// - Type: TimeBonus
// - Target Time: 5 seconds
// - Max Time: 15 seconds
// - Max Points: 50
// - Min Points: 10

// Step3_AimAtFire scoring config:
// - Type: Binary
// - Correct Points: 30 (aimed correctly)
// - Incorrect Points: 0 (missed target)

// Step4_ExtinguishFire scoring config:
// - Type: Accuracy
// - Min Points: 20
// - Max Points: 100
// - (Accuracy set by fire extinguish logic based on coverage)

// ActivityScoring configuration:
// - Include Step Scores: true
// - Aggregation Mode: Sum
// - Activity Scoring Config:
//   - Type: TimeBonus
//   - Target Time: 30 seconds (entire activity)
//   - Max Time: 90 seconds
//   - Max Points: 50 (activity completion bonus)
//   - Min Points: 10

// Custom step that sets accuracy based on fire coverage
using Molca.Sequence;
using MolcaSDK.VR.Scenario.Scoring;
using UnityEngine;

public class ExtinguishFireStep : Step
{
    [SerializeField] private FireSimulation fireSimulation;
    
    private StepScoringAuxiliary _scoring;

    protected override void OnStepActivated()
    {
        base.OnStepActivated();
        _scoring = GetAuxiliary<StepScoringAuxiliary>();
        
        if (fireSimulation != null)
        {
            fireSimulation.OnFireExtinguished += OnFireExtinguished;
        }
    }

    protected override void OnStepDeactivated()
    {
        base.OnStepDeactivated();
        
        if (fireSimulation != null)
        {
            fireSimulation.OnFireExtinguished -= OnFireExtinguished;
        }
    }

    private void OnFireExtinguished(float coveragePercent)
    {
        // Set accuracy based on how much of the fire was covered
        // coveragePercent is 0-100, SetAccuracyPercent expects 0-100
        _scoring?.SetAccuracyPercent(coveragePercent);
        
        Debug.Log($"Fire extinguished with {coveragePercent:F1}% coverage");
        Complete();
    }
}

// Fire simulation component (example)
public class FireSimulation : MonoBehaviour
{
    public event System.Action<float> OnFireExtinguished;
    
    private float _coverage = 0f;

    public void ApplyExtinguisher(Vector3 aimPoint)
    {
        // Simulate coverage based on aim accuracy
        float distance = Vector3.Distance(aimPoint, transform.position);
        if (distance < 0.5f)
        {
            _coverage += Time.deltaTime * 20f; // 20% per second at close range
            _coverage = Mathf.Clamp(_coverage, 0f, 100f);
        }

        if (_coverage >= 80f)
        {
            OnFireExtinguished?.Invoke(_coverage);
        }
    }
}
```

This example demonstrates:

* Multiple scoring types across different steps
* Activity-level time bonus on top of step scores
* Custom step logic that sets accuracy based on performance
* Real-time score updates during fire extinguishing
* Total possible score: 10 + 50 + 30 + 100 + 50 = 240 points (if all maximums achieved)

## Troubleshooting

* **Step scores always zero**: Check that `ScoringType` is not set to `None`. For time-based scoring, verify `targetTimeSeconds` and `maxTimeSeconds` are configured correctly. For `Binary` scoring, ensure `SetCorrect()` or `SetIncorrect()` is called before the step completes.
* **Activity score missing step contributions**: Verify `Include Step Scores` is checked on `ActivityScoring`. Ensure steps are registered in the activity's `SequenceController.Steps` array. Check that `StepScoringAuxiliary` components are attached to step GameObjects.
* **Session posting fails**: Confirm `ScenarioSessionManager` is active and a session is created. Verify `OrgScenarioId`, `org_activity_id`, and `org_step_id` are set in scenario data. Check that `postScoreToSession` is enabled on `StepScoringAuxiliary`. Review console logs for specific error messages.
* **Time-based scores don't update**: Only `TimeBonus`, `TimePenalty`, and `Countdown` types refresh during `OnStepUpdate`. Ensure the step is active and `Update()` is running. For activity-level time scoring, verify the activity is active (`IsActive()` returns true).
* **Binary scoring feels inverted**: `Binary` scoring awards `correctPoints` when accuracy >= 0.5. Use `SetCorrect()` (sets accuracy to 1.0) for correct actions and `SetIncorrect()` (sets accuracy to 0.0) for incorrect actions. Don't manually set accuracy values between 0 and 1 for binary scoring.
* **Negative scores appearing**: Set `allowNegativeScore` to false in `ScoringConfig` to clamp final scores to >= 0. Alternatively, adjust `minPoints` or reduce `pointsPerSecond` for `TimePenalty` scoring to prevent negative results.
* **Max possible score calculation wrong**: `GetMaxPossibleScore()` sums the maximum from each step's `ScoringConfig` plus the activity's base config. It does not include runtime manual offsets from `AddBonusPoints()`. Verify each step's scoring config has correct `maxPoints` or `basePoints` values.

## Related

* [Scoring Config](/vr/scoring/scoring-config) — scoring types, formulas, and configuration reference
* [Step Scoring Auxiliary](/vr/scoring/step-scoring-auxiliary) — step-level scoring API and session posting
* [Activity Scoring](/vr/scoring/activity-scoring) — activity-level score aggregation and events
* [Recipe: Set up a VR scenario](/recipes/setup-vr-scenario) — prerequisite VR scenario setup
* [ScenarioActivity](/vr/scenario-activity) — activity lifecycle and integration
* [Step](/core/step) — step lifecycle and auxiliary system
* [VR step auxiliaries](/vr/steps/vr-step-auxiliaries) — overview of auxiliary components
