Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix duplicate agent creation when reactivating scene #47

Merged
merged 8 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 25 additions & 22 deletions Runtime/AgentDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ namespace DeNA.Anjin
/// <summary>
/// Agent dispatcher interface
/// </summary>
public interface IAgentDispatcher
public interface IAgentDispatcher : IDisposable
{
/// <summary>
/// Agent dispatch by next scene
/// </summary>
/// <param name="current">Current scene</param>
/// <param name="next">Next transition scene</param>
void DispatchByScene(Scene current, Scene next);
/// <param name="next"></param>
/// <param name="mode"></param>
void DispatchByScene(Scene next, LoadSceneMode mode);

/// <summary>
/// Agent dispatch by current scene
Expand All @@ -30,7 +30,7 @@ public interface IAgentDispatcher
void DispatchByScene(Scene scene);
}

/// <inheritdoc />
/// <inheritdoc/>
public class AgentDispatcher : IAgentDispatcher
{
private readonly AutopilotSettings _settings;
Expand All @@ -48,22 +48,21 @@ public AgentDispatcher(AutopilotSettings settings, ILogger logger, RandomFactory
_settings = settings;
_logger = logger;
_randomFactory = randomFactory;
SceneManager.sceneLoaded += this.DispatchByScene;
}

/// <summary>
/// Dispatch agent mapped to Scene `next`
/// </summary>
/// <param name="current"></param>
/// <param name="next"></param>
public void DispatchByScene(Scene current, Scene next)
public void Dispose()
{
SceneManager.sceneLoaded -= this.DispatchByScene;
}

/// <inheritdoc/>
public void DispatchByScene(Scene next, LoadSceneMode mode)
nowsprinting marked this conversation as resolved.
Show resolved Hide resolved
{
DispatchByScene(next);
}

/// <summary>
/// Dispatch agent mapped to Scene
/// </summary>
/// <param name="scene"></param>
/// <inheritdoc/>
public void DispatchByScene(Scene scene)
{
AbstractAgent agent = null;
Expand All @@ -79,17 +78,21 @@ public void DispatchByScene(Scene scene)

if (!agent)
{
if (!_settings.fallbackAgent)
if (_settings.fallbackAgent)
{
_logger.Log($"Use fallback agent. scene: {scene.path}");
agent = _settings.fallbackAgent;
}
else
{
_logger.Log(LogType.Warning, $"Agent not found by scene: {scene.name}");
return;
}

_logger.Log($"Use fallback agent. scene: {scene.path}");
agent = _settings.fallbackAgent;
}

DispatchAgent(agent);
if (agent)
{
DispatchAgent(agent);
}
nowsprinting marked this conversation as resolved.
Show resolved Hide resolved

if (_settings.observerAgent != null)
{
Expand All @@ -107,7 +110,7 @@ private void DispatchAgent(AbstractAgent agent)

agent.Logger = _logger;
agent.Random = _randomFactory.CreateRandom();

try
{
agent.Run(token).Forget();
Expand Down
5 changes: 2 additions & 3 deletions Runtime/Autopilot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Collections;
using System.Threading;
using Cysharp.Threading.Tasks;
using DeNA.Anjin.Reporters;
using DeNA.Anjin.Settings;
using DeNA.Anjin.Utilities;
using UnityEngine;
Expand Down Expand Up @@ -51,7 +50,6 @@ private void Start()

_dispatcher = new AgentDispatcher(_settings, _logger, _randomFactory);
_dispatcher.DispatchByScene(SceneManager.GetActiveScene());
SceneManager.activeSceneChanged += _dispatcher.DispatchByScene;

if (_settings.lifespanSec > 0)
{
Expand Down Expand Up @@ -92,13 +90,14 @@ private IEnumerator Lifespan(int timeoutSec)
/// <param name="exitCode">Exit code for Unity Editor</param>
/// <param name="logString">Log message string when terminate by the log message</param>
/// <param name="stackTrace">Stack trace when terminate by the log message</param>
/// <param name="token">Cancellation token</param>
/// <returns>A task awaits termination get completed</returns>
public async UniTask TerminateAsync(ExitCode exitCode, string logString = null, string stackTrace = null,
CancellationToken token = default)
{
if (_dispatcher != null)
{
SceneManager.activeSceneChanged -= _dispatcher.DispatchByScene;
_dispatcher.Dispose();
}

if (_logMessageHandler != null)
Expand Down
133 changes: 87 additions & 46 deletions Tests/Runtime/AgentDispatcherTest.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,46 @@
// Copyright (c) 2023 DeNA Co., Ltd.
// Copyright (c) 2023-2024 DeNA Co., Ltd.
// This software is released under the MIT License.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Triggers;
using DeNA.Anjin.Agents;
using DeNA.Anjin.Settings;
using DeNA.Anjin.Utilities;
using NUnit.Framework;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
#if UNITY_EDITOR
using UnityEditor.SceneManagement;
#endif

#pragma warning disable CS0618 // Type or member is obsolete

namespace DeNA.Anjin
{
[UnityPlatform(RuntimePlatform.OSXEditor, RuntimePlatform.WindowsEditor, RuntimePlatform.LinuxEditor)]
[SuppressMessage("ApiDesign", "RS0030")]
public class AgentDispatcherTest
{
private IAgentDispatcher _dispatcher;

[SetUp]
public void SetUp()
{
Assume.That(GameObject.Find(nameof(DoNothingAgent)), Is.Null);
foreach (var agent in Object.FindObjectsOfType<AbstractAgent>())
{
Object.Destroy(agent);
}
}

[TearDown]
public async Task TearDown()
public void TearDown()
{
var testAgentObject = GameObject.Find(nameof(DoNothingAgent));
Object.Destroy(testAgentObject);
await Task.Delay(100);
_dispatcher?.Dispose();
}

private static AutopilotSettings CreateAutopilotSettings()
Expand All @@ -47,80 +57,111 @@ private static DoNothingAgent CreateDoNothingAgent(string name = nameof(DoNothin
return doNothingAgent;
}

private void SetUpDispatcher(AutopilotSettings settings)
{
var logger = new ConsoleLogger(Debug.unityLogger.logHandler);
var randomFactory = new RandomFactory(0);

_dispatcher = new AgentDispatcher(settings, logger, randomFactory);
}

private const string TestScenePath = "Packages/com.dena.anjin/Tests/TestScenes/Buttons.unity";
private const string TestScenePath2 = "Packages/com.dena.anjin/Tests/TestScenes/Error.unity";

private static Scene LoadTestScene()
private static async UniTask<Scene> LoadTestSceneAsync(string path, LoadSceneMode mode = LoadSceneMode.Single)
{
return EditorSceneManager.LoadSceneInPlayMode(
TestScenePath,
new LoadSceneParameters(LoadSceneMode.Single));
Scene scene = default;
#if UNITY_EDITOR
scene = EditorSceneManager.LoadSceneInPlayMode(path, new LoadSceneParameters(mode));
while (!scene.isLoaded)
{
await Task.Yield();
}
#endif
return scene;
}

[Test]
public void DispatchByScene_DispatchAgentBySceneAgentMaps()
public async Task DispatchByScene_DispatchAgentBySceneAgentMaps()
{
const string ActualAgentName = nameof(DoNothingAgent);

const string AgentName = "Mapped Agent";
var settings = CreateAutopilotSettings();
settings.sceneAgentMaps.Add(new SceneAgentMap
{
scenePath = TestScenePath, agent = CreateDoNothingAgent()
scenePath = TestScenePath, agent = CreateDoNothingAgent(AgentName)
});
SetUpDispatcher(settings);

var logger = new ConsoleLogger(Debug.unityLogger.logHandler);
var randomFactory = new RandomFactory(0);
var dispatcher = new AgentDispatcher(settings, logger, randomFactory);
dispatcher.DispatchByScene(LoadTestScene());
await LoadTestSceneAsync(TestScenePath);

var gameObject = GameObject.Find(ActualAgentName);
Assert.That(gameObject, Is.Not.Null);
var actual = Object.FindObjectsOfType<AsyncDestroyTrigger>().Select(x => x.name);
Assert.That(actual, Is.EquivalentTo(new[] { AgentName }));
Kuniwak marked this conversation as resolved.
Show resolved Hide resolved
}

[Test]
public void DispatchByScene_DispatchFallbackAgent()
public async Task DispatchByScene_DispatchFallbackAgent()
{
const string ActualAgentName = "Fallback";

const string AgentName = "Fallback Agent";
var settings = CreateAutopilotSettings();
settings.fallbackAgent = CreateDoNothingAgent(ActualAgentName);
settings.fallbackAgent = CreateDoNothingAgent(AgentName);
SetUpDispatcher(settings);

var logger = new ConsoleLogger(Debug.unityLogger.logHandler);
var randomFactory = new RandomFactory(0);
var dispatcher = new AgentDispatcher(settings, logger, randomFactory);
dispatcher.DispatchByScene(LoadTestScene());
await LoadTestSceneAsync(TestScenePath);

var gameObject = GameObject.Find(ActualAgentName);
Assert.That(gameObject, Is.Not.Null);
var actual = Object.FindObjectsOfType<AsyncDestroyTrigger>().Select(x => x.name);
Assert.That(actual, Is.EquivalentTo(new[] { AgentName }));
}

[Test]
public void DispatchByScene_NoSceneAgentMapsAndFallbackAgent_AgentIsNotDispatch()
public async Task DispatchByScene_NoSceneAgentMapsAndFallbackAgent_AgentIsNotDispatch()
{
var settings = CreateAutopilotSettings();
var logger = new ConsoleLogger(Debug.unityLogger.logHandler);
var randomFactory = new RandomFactory(0);
var dispatcher = new AgentDispatcher(settings, logger, randomFactory);
dispatcher.DispatchByScene(LoadTestScene());
SetUpDispatcher(settings);

await LoadTestSceneAsync(TestScenePath);

var actual = Object.FindObjectsOfType<AsyncDestroyTrigger>().Select(x => x.name);
Assert.That(actual, Is.Empty);
LogAssert.Expect(LogType.Warning, "Agent not found by scene: Buttons");
}

[Test]
public void DispatchByScene_DispatchObserverAgent()
public async Task DispatchByScene_DispatchObserverAgent()
{
const string ActualAgentName = "Observer";
const string AgentName = "Observer Agent";
var settings = CreateAutopilotSettings();
settings.observerAgent = CreateDoNothingAgent(AgentName);
SetUpDispatcher(settings);

await LoadTestSceneAsync(TestScenePath);

var actual = Object.FindObjectsOfType<AsyncDestroyTrigger>().Select(x => x.name);
Assert.That(actual, Is.EquivalentTo(new[] { AgentName }));
}

[Test]
public async Task DispatchByScene_ReActivateScene_NotCreateDuplicateAgents()
{
const string AgentName = "Mapped Agent";
var settings = CreateAutopilotSettings();
settings.fallbackAgent = CreateDoNothingAgent();
settings.observerAgent = CreateDoNothingAgent(ActualAgentName);
settings.sceneAgentMaps.Add(new SceneAgentMap
{
scenePath = TestScenePath, agent = CreateDoNothingAgent(AgentName)
});
SetUpDispatcher(settings);

var logger = new ConsoleLogger(Debug.unityLogger.logHandler);
var randomFactory = new RandomFactory(0);
var dispatcher = new AgentDispatcher(settings, logger, randomFactory);
dispatcher.DispatchByScene(LoadTestScene());
var scene = await LoadTestSceneAsync(TestScenePath);

var agents = Object.FindObjectsOfType<AbstractAgent>().Select(x => x.name);
Assume.That(agents, Is.EquivalentTo(new[] { AgentName }));

var additiveScene = await LoadTestSceneAsync(TestScenePath2, LoadSceneMode.Additive);
SceneManager.SetActiveScene(additiveScene);

SceneManager.SetActiveScene(scene); // Re-activate

var gameObject = GameObject.Find(ActualAgentName);
Assert.That(gameObject, Is.Not.Null);
var actual = Object.FindObjectsOfType<AsyncDestroyTrigger>().Select(x => x.name);
Assert.That(actual, Is.EquivalentTo(new[] { AgentName }));
}
}
}