Skip to main content

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.

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

Step-by-step

Step 1: Add StepScoringAuxiliary to steps

Add StepScoringAuxiliary components to individual steps that should contribute to the activity score.
// 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.
// 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.
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.
// 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.
// 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.
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.
// 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:
// 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.