Survival Shooter Link:

Unity Version:

  • Unity 2018.1.5f1

Related Demos:

NOTE:

  • This tutorial is stand alone, but it is also the pre-requisite for syncing objective based tutorials and dialog system.

Uses:

  • You can use this to talk to the Player
  • A conversation between two or more characters.
  • A quick way of sending a message to the player. (ex. in-game instructions)
  • Substituting / Testing NPC Questing.

Summary

The purpose of this Objective View is to display the next available objective, as its picked up. It is a simple implementation of displaying the active item on a list, and to get an understanding of it to either use it as is, or to modify it further for a much more complex objective notification system for the player.

ObjectiveViewTutorial-ObjectiveEventExecution

The above image goes over the following Steps:

  1. The StartEvent will have a reference to the Request Object.
    1. The Request Object will contain information on the Next Event.
  2. This event is somehow invoked by the player character.
    1. Potentially by
      1. entering an area (Collider, or TriggerEvent).
      2. Destroying an object
      3. Entering the Scene
      4. Starting a level.
      5. Starting, or Finishing a Quest, or storyline event.
      6. Etc.
    2. This then starts a chain of events, where the request data is then:
      1. Added to a list of Requests.
      2. Added to the UI Object.
      3. Invokes the OnRequestBegin if any
  3. When OnRequestBegin is invoked, a RequestId had just been added by the PlayerCharacterController, to the Request Object. The EndEvent needs to know what the RequestId is, so when the objective is complete, it can then pass the RequestId to finish the objective.
  4. The player has done some action to finish the event, such as triggering some collider at a destination. Destroying some object, etc.. Assuming the EndEvent has received the RequestId, It can then call the PlayerObjectiveController.CompleteRequest(requestId), and this will complete the given Request Object.


Getting Started…

  1. To Follow Along, Download the Survival Shooter Tutorial.
  • NOTE:

    * This is not required to create the Objective View, we are just using it as a sandbox project to test our objectiveView.
    * If you don’t need the survival shooter, skip to Create ObjectiveView Object Below.

2. Store the Survival Shooter Project in a 0 – Game Folder. This will allow us to create our Demo project in a separate folder, and allow us to use it in any project.

SceneSetup-2-Project

3. Then, create a Demo Folder at the top level. This will be separate from the survival shooter project.

4. Open up the Survival Shooter Project, Located here:

  • 0 – Game/_Complete-Game/_Complete-Game

    NOTE: Normally, we would create a UI Canvas from scratch, by right clicking anywhere in the ‘Hierarchy’, going to UI, then Canvas. However, there’s already a canvas available in this Project, named HUD Canvas. We will use that instead.


Create ObjectiveView Object

  1. Right Click HUD Canvas in the Hierarchy, and select Create Empty. Name it ObjectiveView.
  • This will function as our container for anything related to the ObjectiveView.

2. Right Click our ObjectiveView object in the Hierarchy, and select Create Empty. Name it Panel_BG.

  • Set Width: 350
  • Set Height: 150

3. On the Panel_BG click Add Component at the bottom, and add Grid Layout Group.

  • Set Padding settings:
    • Left: 5
    • Right: 5
    • Top: 10
    • Bottom: 10
  • Cell Size (300, 25)
    • NOTE: You may need to adjust this depending on what you need. The point is to set the dimensions of the column you will use for your Text object later.
  • Constraint Count: 1
    • NOTE: This constraints the Columns to just one column. You can achieve the same effect by using a Vertical Layout Group. However, this gives you more flexibility if you want to adjust it later by adding a second column, such as animated bullets, crossed out objectives, etc. but for this tutorial, we’re going to keep it simple, and avoid the animations for now.

GridLayoutGroup

4. Create a new Text object, by right clicking our PANEL_BG object, in the Hierarchy, go to UI/Text.

  • Set it over the Panel, and rename it Objective_Text.
  • This will be our model Text Object for the ObjectiveView.
    • NOTE: We will replace these later(in step 7 of Coding our Objective Controller) with our own Text Object later.

Coding our Objective Controller

ObjectiveViewTutorial-Code

  1. In our Projects Panel , right click Demo Folder, and create 4 folders and 6 scripts.
  • Folders:
    • Demo/Prefabs/
    • Demo/Scripts/
    • Demo/Scripts/ObjectiveView/
    • Demo/Scripts/ObjectiveView/Events/
  • Scripts:
    • Demo/Scripts/ObjectiveView/PlayerObjectiveView.cs
    • Demo/Scripts/ObjectiveView/PlayerObjectiveController.cs
    • Demo/Scripts/ObjectiveView/ObjectiveText.cs
    • Demo/Scripts/ObjectiveView/Request.cs
    • Demo/Scripts/ObjectiveView/Events/RequestIdEvent.cs
    • Demo/Scripts/ObjectiveView/Events/RequestEvent.cs

2. We will start by first making our RequestIdEvent, RequestEvent, and our Request.cs object, as it’s the easiest, and the rest builds off of this.

  • Our RequestIdEvent is easy, simply make it Serializable, and have it derive from UnityEvent

 

public class RequestIdEvent : UnityEvent { }

 

  • Our RequestEvent is just as easy. Make it Serializable, and have it derive from UnityEvent

public class RequestEvent : UnityEvent { }
  • This will function as our EventObject for any listeners that may need to know the ID of the object, or simply need to know when to invoke.

3. We will then create our Request.cs class, by first making it Serializable, and adding a few fields:

Name

Type

Description

RequestId

Int

The RequestId for this Request Object.

IsAccessible

Bool = true

Is this next event accessible to the player?

IsCurrentlyAccessible

bool

Is this event active yet? The playerObjectiveController will activate the event when it is ready

DisplayText

string

The text that will be displayed on the Objective View Panel.

OnRequestBegin

RequestIdEvent

An event that will be triggered when the request starts.

OnRequestComplete

RequestIdEvent

An event that will be triggered when the request ends.

    • After which, two methods are added. One to Start the Request, and one to complete the Request. The last two requests are invoked by the PlayerObjectiveController after they are added to them.

public void StartRequest() {
   isCurrentlyActive = true;
   if(OnRequestBegin != null)
   {
      OnRequestBegin.Invoke(RequestId);
   }
}
public void CompleteRequest() {
   isCurrentlyActive = false;
   if(OnRequestComplete != null)
   {
      OnRequestBegin.Invoke(RequestId);
   }
}

4. Our next class is the  PlayerObjectiveController.cs. This will be a ScriptableObject that will be used to store our objective requests.  This will lead into our PlayerObjectiveView.cs

    • With that being said, we will add a CreateAssetMenu reference to the script.
[CreateAssetMenu(fileName = "PlayerObjectiveController", menuName = "Fuzzy/ObjectiveController", order = 1)]
public class PlayerObjectiveController : ScriptableObject {
    • The CreateAssetMenu reference will allow us to create a ScriptableObject in our Project Panel, from our right click menu.

CreateObjectiveControllerPath

5.) Now we need a List of our objective requests, and an index that will be shared between objects to reference the request. We can do this by creating a Dictionary, with a key value as the index, and the value as our Request.

private int _index = 0;
private Dictionary<int, Request> _requests = new Dictionary<int, Request>(); 
public Dictionary<int, Request> requests { get { return _requests; } }
  • The Get Request is available in case we need the list of requests, but we don’t want outside sources to modify the main list.
  • Lets add 2 events to the PlayerObjectiveController.cs for any listeners that may need to know whenever a new event is added to the player’s objective controller.

Name

Type

Description

OnRequestAdd

RequestEvent

Invoked whenever a new Request is added.

OnRequestComplete

RequestIdEvent

Invoked whenever a Request has been completed.

6. Now with our List of Requests added, we need to add 2 new methods. An AddNewRequest(Request) method, and a CompleteRequest(int) method.

Method

Description

AddNewRequest(Request)

Will be used to add new Requests to the PlayerObjectiveController.

CompleteRequest(int)

When a new request is completed, the requestID is fed. This will close the request, and update any associated events.

public int AddNewRequest(Request request = null) 
{
   if (!_requests.Any(x => x.Key == request.RequestId))
   {
      _index++;
      _requests.Add(_index, request);
      request.RequestId = _index;
      request.StartRequest();
      if(OnRequestAdd != null)
      OnRequestAdd.Invoke(request);
   }
   else 
   {
      throw new UnityException("Fuzzy - ObjectiveController.AddNewRequest : This request is already added");
   }

   return _index;
}

First we check if the RequestId already exists, or it’s already added. If so, then we simply reject it, as we don’t need to re-add the same request back into the pool.

NOTE: In here we handled it by throwing an exception, but there are other ways of handling this too. Maybe you want to make the quest repeatable, in which case that could be a parameter in the Request Mechanics.

Then we increment the index, as we are treating this as a sort of dataTable, and add the new request to the list.

Invoke the Request.StartRequest(), and invoke the OnRequestAdd.Invoke(Request), and return the value given to the Request.

  • The OnRequestAdd.Invoke functions as our way to tell any objects that needs to know that a new request has been added, whether or not they need the actual Request object Info. In our case however, we just need the PlayerObjectiveView to know about our new request added.

For our CompleteRequest, we want to do something similar, but instead of adding it, we want to mark it as complete.

public void CompleteRequest(int requestId)
{
   var req = _requests.FirstOrDefault(x => x.Key == requestID);

   //Find request
   if(_requests.Any(x => x.Key == requestID)) 
   {
      //Invoke Events
      req.Value.CompleteRequest();
      if(OnRequestComplete != null)
      OnRequestComplete.Invoke(req.Key);
}
}

Now in our PlayerObjectiveController.CompleteRequest, we first feed it the requestID we want to close, or finish, and check if it exists in our list. If it does, exist, invoke the Request.CompleteRequest() method, and invoke the OnRequestComplete.Invoke(req.key) method.

  • Here we can check for certain events, and mark them off of our playerObjectiveView.

    For Example: the doors in super mario 64, and paintings. Whenever mario finished a star within a painting, that star would be claimed. Also, one of the main doors, wouldn’t open until mario had obtained at least 25 stars. We can have a method listening to our OnRequestComplete for events like this counting the number of stars completed.

7. Before we get into our PlayerObjectiveView.cs, lets write our ObjectiveText.cs class.

This will contain a single method, and will derive from UnityEngine.UI.Text class.

  • Add a key reference, so we know which key objective it is using, and an UpdateObjective(Request) method.
public class ObjectiveText : Text {
   public int Key;

   public void UpdateObjective(Request request) {
      Key = request.RequestId;
      this.text = request.displayText;
      this.gameObject.SetActive(true);
   }
}
  • Update the key, to contain the request.RequestId.
  • Assign the text that will be displayed, to the requests displayText.
  • Then activate the Text Object.
  • That’s it!
  • This will be used as a substitute Text component. Alternatively you can have ObjectiveText inherit from MonoBehaviour, and add a reference to Text instead. Both methods work just fine.

8. In the PlayerObjectiveView.cs we will need a few references.

  • We need a reference to our objectivePanelBG, our PlayerObjectiveController, and a modelTextObject.
  • We also need a list of all ObjectiveText objects at our disposal, to use, and re-use them as we sit fit.

Name

Type

Description

playerObjective

PlayerObjectiveController

A reference to our playerObjectiveController

ObjectivePanelBG

GameObject

The visual background panel for our playerObjectiveView.

modelObjectiveText

ObjectiveText

A visual ModelObjectiveText reference.

currentObjectiveView

List

A list of ObjectiveTexts ready at our disposal.

NOTE: Be sure to assign these values in your inspector. Also, if you’re familiar with ZenJect, the playerObjective can be inejcted from here.

  • Now, in this script, we will have 3 methods. 2 of which, will be listening to our PlayerObjectiveController.OnRequestAdd, and PlayerObjectiveController.OnRequestComplete. The third method is ObjectiveText[] GetObjectives() { … }.

9. Let’s start with our GetObjectives() method. This method is used to get a list of our objectives at our disposal, to use, and recycle.

private ObjectiveText[] GetObjectives() 
{
   return ObjectivePanelBG.GetComponentsInChildren(true);
}
  • This simply gets a list of child ObjectiveText’s from our ObjectivePanelBG. Also, because of the GridLayoutGroup attached earlier, this will be displayed in a nice fashion on the UI.
    • !NOTE: You can replace this with an object pooling system, but to keep the code simple, I recommend keeping the signature as is to keep changes simple.
  • Next, we need our method Listeners. First is our AddNewUIRequest(Request) method, that will listen to the request info, and activate it in the ObjectiveView from the list.
public void AddNewUIRequest(Request request) 
{
   ObjectiveText obj = currentObjectiveView.FirstOrDefault(x => !x.isActiveAndEnabled);
   if(obj != null)
   {
      obj.UpdateObjective(request);
   }
   else
   {
      ObjectiveText modelText = GameObject.Instantiate(modelObjectiveText);
      modelText.transform.SetParent(ObjectivePanelBG.transform);
      modelText.enabled = false;
      currentObjectiveView.Add(modelText);
      AddNewUIRequest(request);
   }
}
  • Our AddNewUIRequest(Request request) will receive a request, and check from the list of currentObjectiveView for any disabled, or inactive objectives.
    • IF IT RETURNS AN OBJ, we will feed it the given Request object, and we’re done.
    • ELSE, it will create a new one from the given model, assign its parent to ObjectivePanelBG.transform. Disable the model, add it to the list of currentObjectiveView, and do a re-cursive call to AddNewUIRequest.
      • !NOTE: if you have an object pooling implementation, it would be safe to add it to the else method here, and do a recursive call.
public void DisableUIRequest(int requestId) 
{
   ObjectiveText objText = currentObjectiveView.FirstOrDefault(x => x.Key == requestId);
   objText.gameObject.SetActive(false);
}
  • The DisableUIRequest(int) method will be listening to our OnComplete event in our ObjectiveController.
    • It’s responsibility is to find the active request in our objectiveView, and simply deactivate it. This will make the object eligible for the next incoming request.
public void Start() {
   currentObjectiveView = new List();
   playerObjectives.OnRequestAdd.AddListener(AddNewUIRequest);
   playerObjectives.OnRequestComplete.AddListener(DisableUIRequest);

   //Get a collection of objectives.
   currentObjectiveView.AddRange(GetObjectives());

   //Disable all objectives at the start of the game.
   currentObjectiveView.ForEach(x => x.gameObject.SetActive(false));
}
  • Now in our void Start() method, we just need to tie everything together.
    • Call the playerObjectives, and add a listener from OnRequestAdd to our AddNewUIRequest
    • Call the playerObjectives, and add a listener from OnRequestComplete to our DisableUIRequest
    • Get a list of currentObjectiveView from our GetObjectives() method
      • And disable each method.
      • !NOTE: you may need a using System.Linq reference.

Starting & Ending our Requests!

We now have a fully functional event system! All we need to do now, is feed it requests to start them, and complete it whenever we’re done with them.

Let’s start by making two scripts:

  • Demo/Scripts/ObjectiveView/StartObjective.cs
  • Demo/Scripts/ObjectiveView/CompleteObjective.cs

StartObjective

In StartObjective, add a reference to PlayerObjectiveController, and a reference to Request.

When the character triggers the objective, we need it to call the PlayerObjectiveController.AddNewRequest(request);

public PlayerObjectiveController objectiveController;

public Request request;

public void OnTriggerEnter(Collider collider) 
{
   objectiveController.AddNewRequest(request);
}

NOTE: With ZenJect, you can inject the PlayerObjectiveController on every object instance that you need.

The StartObjective will be attached to a boxCollider on the scene. When the character enters the box collider, it will trigger the objective, start the new event, and start the objective.

CompleteObjective

In CompleteObjective, add a reference to PlayerObjectiveController, and a requestId reference.

public int requestId;

public PlayerObjectiveController objectiveController;

public void AssignRequestId(int id)
{
   requestId = id;
}

public void OnTriggerEnter(Collider collider) 
{
   objectiveController.CompleteRequest(requestId);
}

AssignRequestId will be invoked when the objective is Started through StartObjective.Request.OnRequestBegin. We can add this reference in the inspector.

OnTriggerEnter is invoked, when the player enters this destination, and will Complete the given Request.


Editor

In the Editor we will Create a cube for the character to Trigger. This will start the objective. Should look something like this:

Inspector - StartObjective In the inspector, the MeshRenderer is turned off.

Our StartObjective contains a reference to our PlayerObjectiveController.

On Request.OnRequestBegin we activate our Destination object, and call the CompleteObjective.AssignRequestId that we created. (Note: Be sure to call this dynamically).

On Request.OnRequestComplete we deactivate our destination object, and re-activate our EnemyManager.
NOTE: The EnemyManager was deactivated before game start,  to prevent enemies from spawning, while testing our Objective System.

Our Destination object will be much simpler:

Inspector - EndObjective

A reference to our PlayerObjectiveController is added to our CompleteObjective script.

The only other important thing to note, is to deactivate the destination object, and have the Request.OnStartRequest activate it when the request begins.

Happy Developing!