I have been continuing with that goal of making an initial mission where the player has to learn to use skills, heal, capture creatures, etc to serve as an integrated tutorial.
My first approach was to create a system that could read from scriptable objects and set up goals for the player. I created a quite a lot of classes, from "Milestones" which represented each step in the mission to the goals and results of achieving milestones. Those were abstract classes and then there were the specific classes for each type of goal: For example for the "eliminate X amount of enemies" goal, activating the goal would spawn the creatures to destroy and subscribe to the "creature death" event to keep track.
public abstract class MilestoneGoalBase
{
protected MilestoneGoalData myGoalData;
protected bool done = false;
public delegate void GoalAchieved();
public event GoalAchieved GoalAchievedEvent;
public static MilestoneGoalBase CreateMilestone(MilestoneGoalData data)
{
MilestoneGoalBase milestoneGoal = null;
foreach (Type type in
Assembly.GetAssembly(typeof(MilestoneGoalBase)).GetTypes()
.Where(myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf(typeof(MilestoneGoalBase))))
{
MilestoneGoalBase mgb = (MilestoneGoalBase)Activator.CreateInstance(type);
if (mgb.GetGoalType() == data.goalType)
{
milestoneGoal = mgb;
milestoneGoal.ActivateGoal(data);
break;
}
}
if (milestoneGoal == null)
Debug.LogError("Attempting to create an Objective Milestone Goal with a MilestoneGoalType with no matching MilestoneGoal implementation.");
return milestoneGoal;
}
protected void CallGoalAchievedEvent()
{
GoalAchievedEvent.Invoke();
}
public abstract MilestoneGoalType GetGoalType();
public void ActivateGoal(MilestoneGoalData data)
{
myGoalData = data;
ActivateGoal();
}
protected abstract void ActivateGoal();
public abstract bool CheckGoal();
}
public class MilestoneGoalDefeatEnemy : MilestoneGoalBase
{
private GameObject enemyToDefeat;
public override MilestoneGoalType GetGoalType()
{
return MilestoneGoalType.DEFEAT_ENEMY;
}
protected override void ActivateGoal()
{
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
enemyToDefeat = Array.Find(enemies, (x) => x.name == myGoalData.goalTargetName);
CreatureHitPoints enemyHitPoints = enemyToDefeat.GetComponent<CreatureHitPoints>();
enemyHitPoints.KnockOutEvent += EnemyHitPoints_KnockOutEvent;
}
~MilestoneGoalDefeatEnemy()
{
if (enemyToDefeat != null)
{
CreatureHitPoints enemyHitPoints = enemyToDefeat.GetComponent<CreatureHitPoints>();
if (enemyHitPoints != null)
enemyHitPoints.KnockOutEvent -= EnemyHitPoints_KnockOutEvent;
}
}
private void EnemyHitPoints_KnockOutEvent()
{
done = true;
CallGoalAchievedEvent();
}
public override bool CheckGoal()
{
return done;
}
}
I lost my patience after a week or so and probably I would come back to complete that in the future, but for now I am taking a more blunt aproach and there is a script called "tutorial.cs" and it does what specifically must be done for the tutorial, no flexibility or reuse potential.
public class TutorialManager : MonoBehaviour
{
[Header("StartingPacklings")]
[SerializeField] private List<Specimen> startingPacklings;
[Header("Conversations")]
[SerializeField] private NPC GuideNPC;
[SerializeField] private Dialogue.Dialogue defeatEnemiesDialogue;
[SerializeField] private Dialogue.Dialogue healAllyDialogue;
[Header("Enemies to defeat")]
[SerializeField] private Specimen enemyToDefeatSpecimen;
[SerializeField] private int enemyQuantity;
private List<CreatureHitPoints> enemiesToDefeat = new List<CreatureHitPoints>();
[SerializeField] Vector3 enemiesSpawnPosition;
[SerializeField] private GameObject baseEnemy;
[Header("Ally to heal")]
[SerializeField] private Specimen allyToHeal;
private void OnEnable()
{
CreatureStorage.activePack.Clear();
foreach (GameObject creature in GameManager.playerCreatures)
Destroy(creature);
GameManager.playerCreatures.Clear();
foreach (Specimen packling in startingPacklings)
CreatureStorage.AddSpecimen(packling, Vector3.zero);
GameManager.SelectPlayer(GameManager.playerCreatures[0]);
GuideNPC.SetDialogue(defeatEnemiesDialogue);
DialogManager.Ref.DialogEndedEvent += OnGuideDefeatEnemiesDialogueEnded;
}
private void OnGuideDefeatEnemiesDialogueEnded()
{
SpawnEnemies();
DialogManager.Ref.DialogEndedEvent -= OnGuideDefeatEnemiesDialogueEnded;
}
public void SpawnEnemies()
{
enemiesToDefeat.Clear();
for (int i = 0; i < enemyQuantity; i++)
{
EnemyTier tier = EnemyTier.FAINT;
GameObject newEnemy = CreatureFactory.CreateCreature(enemyToDefeatSpecimen, baseEnemy, enemiesSpawnPosition, i, tier);
CreatureHitPoints newEnemyHitPoints = newEnemy.GetComponent<CreatureHitPoints>();
enemiesToDefeat.Add(newEnemyHitPoints);
newEnemyHitPoints.KnockOutEvent += OnEnemyKnockOut;
}
}
private void OnEnemyKnockOut()
{
bool allDead = true;
print(allDead);
foreach (CreatureHitPoints creatureHitPoints in enemiesToDefeat)
{
print("BBB");
if (creatureHitPoints != null && creatureHitPoints.currentHP >= 0)
{
print("AAAA");
allDead = false;
break;
}
}
if (allDead)
{
GuideNPC.SetDialogue(healAllyDialogue);
DialogManager.Ref.DialogEndedEvent += OnGuideHealPackMateDialogueEnded;
}
}
private void OnGuideHealPackMateDialogueEnded()
{
SpawnAllyToHeal();
DialogManager.Ref.DialogEndedEvent -= OnGuideHealPackMateDialogueEnded;
}
public void SpawnAllyToHeal()
{
CreatureStorage.AddSpecimen(allyToHeal, GuideNPC.transform.position + new Vector3(10f,0f,10f) );
CreatureHitPoints allyHitPoints = GameManager.playerCreatures[GameManager.playerCreatures.Count - 1].GetComponent<CreatureHitPoints>();
allyHitPoints.consciousnessRecoveryRate = 0f;
allyHitPoints.ConsciousnessChangedEvent += AllyHitPoints_ConsciousnessChangedEvent;
IEnumerator KillPackling()
{
yield return new WaitForSeconds(0.2f);
allyHitPoints.Die();
}
StartCoroutine(KillPackling());
}
private void AllyHitPoints_ConsciousnessChangedEvent(float health, float maxHealth)
{
CreatureHitPoints allyHitPoints = GameManager.playerCreatures[GameManager.playerCreatures.Count - 1].GetComponent<CreatureHitPoints>();
if (allyHitPoints.consciousness > 0)
{
allyHitPoints.RecoverConsciousness(100000);
allyHitPoints.consciousnessRecoveryRate = 0.1f;
}
}
}
I still can salvage code from this if I decide to do the more powerful mission system, but right now I lack the motivation to spend time and energy on that.
This is still not that simple, the hardest part of making a tutorial, IMO, is to avoid messing up your perfectly functional regular situation code. Once you start having "
if (tutorial) " in your mechanics scripts, you are in for a debugging nightmare.
I only made a couple of functions "public", but I am not even confortable with that. What is more, I used a very sketchy solution to getting the player creatures I wanted on the field and working.
Usually the GameManager will spawn the player creatures after getting the information it needs from the CreatureStorage script, that manages loading and saving the creatures to the save game. But I want to create one of those typical RPG scenes where the player controls characters different from the ones saved in their game. So the script has to somehow change the creatures. What I am currently doing is accessing data from the GameManager that by all means should be private or at least public get private set. Then I tell the GameManager to spawn other creatures.
Other similar problems in avoiding the payer defeating the enemies or healing the ally before the time comes for them to do that. Right now the player can not do it because those entities are only spawned when they are needed, but that is not pretty. I might create some roadblocks so the enemies can not be reached or something like that.