Callback runs twice after re-enabling UI
The listener was added repeatedly without removal. Pair AddListener/RemoveListener and inspect persistent Inspector listeners separately.
Scripting
Subscribe and unsubscribe to Manager and Chapter notifications without duplicate listeners or stale singleton references.
| Need | Prefer | Context |
|---|---|---|
| Inspector response to readiness/load/failure | Manager UnityEvents | Parameterless |
| Failure reason and affected objects | ChapterFailed | ChapterFailureInfo |
| Accepted Objective index before actions | Chapter.ObjectiveCompleted | int index |
| Scene detector accepted completion | Objective.OnCompleted | Component-local, after Chapter acceptance |
| Chapter/Objective configured lifecycle | Serialized row events | Authored in product window |
Cache the exact publisher instance, subscribe once, and remove the same delegate before discarding it. A later singleton lookup may return null or a replacement instance during scene teardown. For Chapter-specific events, detach from the old Chapter before observing a newly selected one.
Use OnEnable/OnDisable for scene components whose enabled lifetime matches observation. A persistent integration may instead subscribe after scene load and explicitly rebind when Managers change. Never add an anonymous lambda that cannot be removed unless the publisher has the same or shorter guaranteed lifetime.
Subscriptions receive future transitions only. After attaching, read current public properties once to render the initial state. This is especially important after persistence hydration, which intentionally does not replay historical completion or failure.
OnManagerReady means startup selection was attempted, not that it succeeded. Check CurrentChapter or HasActiveChapter in the listener. OnBeforeSceneLoad means the Manager already locked the transition; keep the callback short and synchronous.
The listener was added repeatedly without removal. Pair AddListener/RemoveListener and inspect persistent Inspector listeners separately.
A persistent receiver may still reference the old Manager. Unsubscribe before unload and bind to the new instance.
Expected: hydration does not replay them. Read state after OnManagerReady.
The Chapter may already have advanced; use the typed pre-action event or capture the ID before calling completion.
using System;
using System.Collections;
using System.Collections.Generic;
using OverFuture.ChapterObjectiveSystem.Chapters;
using OverFuture.ChapterObjectiveSystem.Events;
using OverFuture.ChapterObjectiveSystem.Objectives;
using OverFuture.ChapterObjectiveSystem.Persistence;
using OverFuture.ChapterObjectiveSystem.Progression;
using UnityEngine;
public sealed class LifecycleSubscriptionExample : MonoBehaviour
{
private ChapterManager manager;
private Chapter observedChapter;
private void OnEnable()
{
manager = ChapterManager.Instance;
if (manager == null)
{
return;
}
manager.OnManagerReady.AddListener(HandleManagerReady);
ObserveCurrentChapter();
}
private void OnDisable()
{
manager?.OnManagerReady.RemoveListener(HandleManagerReady);
StopObservingChapter();
manager = null;
}
private void HandleManagerReady()
{
ObserveCurrentChapter();
}
private void ObserveCurrentChapter()
{
StopObservingChapter();
observedChapter = manager != null ? manager.CurrentChapter : null;
if (observedChapter != null)
{
observedChapter.ObjectiveCompleted += HandleObjectiveCompleted;
}
}
private void StopObservingChapter()
{
if (observedChapter != null)
{
observedChapter.ObjectiveCompleted -= HandleObjectiveCompleted;
}
observedChapter = null;
}
private void HandleObjectiveCompleted(int objectiveIndex)
{
Debug.Log($"Completed objective index: {objectiveIndex}", this);
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using OverFuture.ChapterObjectiveSystem.Chapters;
using OverFuture.ChapterObjectiveSystem.Events;
using OverFuture.ChapterObjectiveSystem.Objectives;
using OverFuture.ChapterObjectiveSystem.Persistence;
using OverFuture.ChapterObjectiveSystem.Progression;
using UnityEngine;
public sealed class FailureSubscriptionExample : MonoBehaviour
{
private ChapterManager manager;
private void OnEnable()
{
manager = ChapterManager.Instance;
if (manager != null)
{
manager.ChapterFailed += HandleChapterFailed;
}
}
private void OnDisable()
{
if (manager != null)
{
manager.ChapterFailed -= HandleChapterFailed;
}
manager = null;
}
private void HandleChapterFailed(ChapterFailureInfo information)
{
Debug.Log($"{information.Chapter?.Identifier}: {information.Reason}", this);
}
}