heya folks, welcome to another episode of ‘Bahaa and his programming issues’
In today’s problem, we got an interesting one. I’m trying to convert what I learned from this tutorial (it’s something I asked for, over a year ago) to a state machine, to match the new third person state machine system
After my last update of that script well over 6 months ago, this is what I ended up with (P.S: Ignore anything relative to the ‘EnemyStateMachine.cs’, anything with datatype ‘AIController.cs’ was working perfectly fine…):
using System.Collections.Generic;
using GameDevTV.Saving;
using RPG.Attributes;
using RPG.Control;
using UnityEngine;
using RPG.Combat;
using Newtonsoft.Json.Linq;
using RPG.Dialogue;
using RPG.States.Enemies;
namespace RPG.Respawnables
{
// Since SaveableEntity relies on the ISaveable information of each character to
// bundle it into our Save file, there are cases when our enemies' state is non-existant
// hence we need to create a brand new RespawnManager class, as shown below:
public class RespawnManager : SaveableEntity, IJsonSaveable //, ISaveable in Saving below, we will have to replace the logic, hence we inherit from 'SaveableEntity.cs'
{
// This class will handle the following:
// spawning our enemy to the scene
// listen to death notifications (i.e: Enemy is dead, proceed to keeping him dead for the following steps)
// hide the body after 'hideTime'
// deleting the enemy after 'hideTime' is complete
// Respawn the enemy after 'respawnTime'
// [SerializeField] AIController spawnableEnemy; // the enemy to spawn/respawn
// [HideInInspector] private AIController lastSpawnableEnemy; // this AI Controller ensures its an enemy we are respawning
// Changing 'AIController.cs' above to State Machines:
[SerializeField] EnemyStateMachine spawnableEnemy;
[HideInInspector] private AIController lastSpawnableEnemy;
[SerializeField] private float hideTime = 60; // time before hiding our dead character
[SerializeField] private float respawnTime = 90; // time before respawning our hidden dead character, as an alive character
[SerializeField] PatrolPath patrolPath; // the path our character will follow, from his spawn point
[SerializeField] AggroGroup aggroGroup; // aggrevated group of guards, based on wrong dialogue player has said
[SerializeField] bool hasBeenRestored; // checks if the enemy has been restored before fading in from the main menu, or a load scene, or not
// TEST Variables: Delete if failed
/* [SerializeField] AggroGroup aggroGroup; // the group of enemies we want attacking our player, if things go bad
[SerializeField] DialogueAggro dialogueAggro; */
// private AIController spawnedEnemy; // changed to EnemyStateMachine type below
private EnemyStateMachine spawnedEnemy;
// TEST CODE:
private bool isConversant;
private PlayerConversant playerConversant;
/* void Awake() {
playerConversant = playerConversant.GetComponent<PlayerConversant>();
isConversant = TryGetComponent(out AIConversant conversant);
} */
// --------------------------- NOTE: RestoreState() occurs BEFORE Start(), hence we need to ensure everything works accordingly --------------
private void Start()
{
// Check if the Enemy has been restored first or not, prior to Respawning him (ensuring RestoreState(), which occurs first, works properly)
if (!hasBeenRestored) Respawn();
}
private void Respawn()
{
if (spawnedEnemy)
{
// Dude is not dead no longer, so delete his previous 'onDeath' record after he's respawned
spawnedEnemy.GetComponent<Health>().onDie.RemoveListener(OnDeath);
}
foreach (Transform child in transform)
{
// Start the Respawn by deleting any existing gameObjects
Destroy(child.gameObject);
}
// Respawn the enemy, and parent the enemy to our respawnManagers' transform
spawnedEnemy = Instantiate(spawnableEnemy, transform);
// Get the spawned/respawned enemies' health, and listen for death notifications
spawnedEnemy.GetComponent<Health>().onDie.AddListener(OnDeath);
if (patrolPath != null)
{
Debug.Log($"Assigning Patrol Path {patrolPath} to {spawnedEnemy.name}");
// spawnedEnemy.AssignPatrolPath(patrolPath); // Swapped to state machine on next line
spawnedEnemy.SwitchState(new EnemyPatrolState(spawnedEnemy));
// Update();
}
else
{
Debug.Log($"No Patrol Path to assign");
}
// --------------------------- Extra Functionality: Setting up Aggro Group + Adding Fighters ---------------
// First add the fighters, and then the Dialogue Guard (if there's one), because doing the opposite
// means that we will not be adding fighters if there' no dialogue guard (which is not something we
// want because we need to prioritize fighters over dialogue)
if (aggroGroup != null)
{
aggroGroup.AddFighterToGroup(spawnedEnemy.GetComponent<Fighter>());
if (spawnedEnemy.TryGetComponent(out DialogueAggro dialogueAggro)) //aggrogroup is at this point valid
{
dialogueAggro.SetAggroGroup(aggroGroup);
}
}
// ---------------------------------------------------------------------------------------------------------
}
void HideCharacter()
{
// Hide the dead character
foreach (Renderer renderer in spawnedEnemy.GetComponentsInChildren<Renderer>())
{
renderer.enabled = false;
}
}
void OnDeath() {
// hide the character after 'hideTime', and then respawn him after 'respawnTime'
Invoke(nameof(HideCharacter), hideTime);
Invoke(nameof(Respawn), respawnTime);
// Test (Delete if failed): disable the spawnableEnemy, until he comes back to life
// spawnableEnemy.enabled = false;
// TEST: Delete if failed
if (aggroGroup != null) {
aggroGroup.RemoveFighterFromGroup(spawnedEnemy.GetComponent<Fighter>());
}
// if he quit the game and returned, deactivate the dead dude until he can respawn
}
public JToken CaptureAsJToken()
{
JObject state = new JObject();
IDictionary<string, JToken> stateDict = state;
foreach (IJsonSaveable JSONSaveable in spawnedEnemy.GetComponents<IJsonSaveable>())
{
JToken token = JSONSaveable.CaptureAsJToken();
string component = JSONSaveable.GetType().ToString();
Debug.Log($"{name} Capture {component} = {token.ToString()}");
stateDict[JSONSaveable.GetType().ToString()] = token;
}
return state;
}
public void RestoreFromJToken(JToken s)
{
JObject state = s.ToObject<JObject>();
IDictionary<string, JToken> stateDict = state;
foreach (IJsonSaveable jsonSaveable in spawnedEnemy.GetComponents<IJsonSaveable>())
{
string component = jsonSaveable.GetType().ToString();
if (stateDict.ContainsKey(component))
{
Debug.Log($"{name} Restore {component} => {stateDict[component].ToString()}");
jsonSaveable.RestoreFromJToken(stateDict[component]);
}
}
}
/* private void OnValidate()
{
// This function checks if we changed the Spawnable Character
// (ensuring any character we attach to the spawnableEnemy is automatically added
// to the RespawnManager when editing the game,
// hence we dont accidentally spawn the wrong dude at the right spot)
if (spawnableEnemy != lastSpawnableEnemy)
{
lastSpawnableEnemy = spawnableEnemy;
foreach (Transform child in transform) {
Destroy(child.gameObject);
}
Instantiate(spawnableEnemy, transform);
}
} */
}
}
// THE ARCHER AND ENEMY 2 ARE BOTH CHILDREN OF THE 'CHARACTER' PREFAB. DISCONNECT THEM FROM THAT, OTHERWISE THEY'LL KEEP INSTANTIATING
// FROM UNDER THE QUEST GIVER, WHO IS ALSO A PREFAB FROM THE CHARACTER PREFAB!!!
Over the past few hours, I tried converting it to a State Machine, but because it’s inheriting from an EnemyBaseState instead of a State Machine, this is… a bit of a headache:
using RPG.Attributes;
using RPG.Core;
using UnityEngine;
namespace RPG.States.Enemies {
public class EnemyRespawnState : EnemyBaseState
{
private EnemyStateMachine spawnedEnemy;
public float hideTime; // time before the character hides off the game scene
public float respawnTime; // time before the character respawns as a new alive enemy to fight with
// we have a patrol path in 'EnemyStateMachine.cs'
public EnemyRespawnState(EnemyStateMachine stateMachine, float hideTime, float respawnTime) : base(stateMachine)
{
this.hideTime = hideTime;
this.respawnTime = respawnTime;
}
public override void Enter() {}
public override void Tick(float deltaTime)
{
hideTime -= deltaTime;
if (hideTime <= 0f)
{
HideCharacter();
stateMachine.SwitchState(new EnemyDeathState(stateMachine));
respawnTime -= deltaTime;
}
if (respawnTime <= 0f)
{
Respawn();
stateMachine.SwitchState(new EnemyPatrolState(stateMachine));
}
else respawnTime -= deltaTime;
}
public override void Exit() {}
private void HideCharacter()
{
foreach (Renderer renderer in stateMachine.gameObject.GetComponentsInChildren<Renderer>())
{
renderer.enabled = false;
}
}
private void Respawn()
{
if (spawnedEnemy != null)
{
spawnedEnemy.GetComponent<Health>().onDie.RemoveListener(OnDeath); // he's dead, don't need to listen for his death anymore...
}
foreach (Transform child in stateMachine.transform)
{
// find a way to destroy the dead enemy without having to use the 'Destroy()' function
GameObject.Destroy(child.gameObject);
}
// Respawn the enemy, and parent it to the respawnManager's transform:
// spawnedEnemy = GameObject.Instantiate(stateMachine.SpawnableEnemy, stateMachine.transform)
// spawnedEnemy = spawnedEnemy.GetComponent<EnemyStateMachine>();
// He's alive again, start listening for his death all over again...
spawnedEnemy.GetComponent<Health>().onDie.AddListener(OnDeath);
// Assign possible Patrol Path
if (stateMachine.PatrolPath != null)
{
spawnedEnemy.SwitchState(new EnemyPatrolState(spawnedEnemy));
}
// AggroGroup logic here... (will need reworking down the line, so it's empty for now)
}
private void OnDeath()
{
// Find a way to invoke both 'HideCharacter' and 'Respawn' functions here,
// and then remove your fighter from 'AggroGroup.cs' (below is usually how it would go):
// Invoke(nameof(HideCharacter), hideTime);
// Invoke(nameof(Respawn), respawnTime);
}
}
}
Can someone kindly have a look at this, and update me on how we can get that missing script to work? (Don’t need the code, but at least some guidance of how this should work would be nice, and I’ll do my best to try figure out programming it myself… above all else, can you integrate the saving system into a State Machine? The original script has it, don’t want to mess that up)