Made in July 2024 using Unity and C#.
On this page
Game Page
Highlights
-
A choice-based narrative-driven game where you interact with characters to
progress in the story and uncover a sinister mystery.
-
Gauge the flow of the conversation and choose the correct response to
advance.
-
Paying close attention helps make the correct choice.
-
Also features 2 minigames closely tied-in to the story for novelty and
variety.
- Two version of this game were developed for my Master's degree dissertation, where we studied the effects of juiciness in narrative-driven games.
Description
In Blindsided, players will play as Jared, a schoolboy, who seeks to uncover the story behind his father’s disappearance. Jared must befriend strangers, converse skilfully, and display politeness, aggression, tact, and diplomacy at the right time to gather information that will bring the culprits of his father to justice.To get information out of other characters, the player, while controlling Jared, must make the right dialogue choices to advance the story. In addition, Jared will be involved in a fist fight and a covert operation to break into a digital safe. The player will utilise their gaming skills to achieve these feats in a couple of minigames before finally getting to the truth. But even after finding what he sought, will Jared get closure?
Technical Walkthrough
Dialogue System
- Being a narrative-driven game, the dialogue system was the most important and the first to be developed.
-
The player's dialogues, which appear as buttons in the game, were
implemented as a structure in the code, with the following fields:
- dialogueId: An enum field to uniquely identify each dialogue.
- playerStates: A list of enums that describes the player states in which the dialogue is accessible. If the player's current state is not present in this list, then the dialogue button will not appear.
- dialogueText: The text of the dialogue stored as a string.
- stateToResponseMap: A Dictionary mapping each state in the playerStates list to an Action. When a dialogue button is pressed, the Action corresponding to the player's state is invoked.
- dialogueAction: An Action variable to store the action to execute when the dialogue button is pressed.
[System.Serializable] public struct SDialogue { public EDialogueID dialogueId; public List<PlayerStates> playerStates; public string dialogueText; public Dictionary<PlayerStates, Action> stateToResponseMap; public Action dialogueAction; }; -
The DialogueManager class handles the storage and fetching of dialogues
based on ID or player state. Each NPC extends this class and populates
its list of dialogues and associated details:
private void FrndDontUWorkAtLettingsNeutralAction() { string[] dialogueList = { "How do you know that? Have you been stalking me?" }; audioController.PlaySound(howDoUKnowVoice); uiController.StartDialogues(dialogueList, FRIEND_CHAR, OnDialogueEnd); } private void FrndDontUWorkAtLettingsJobRvldAction() { string[] dialogueList = { "Yeah. How about you, are you working anywhere?" }; audioController.PlaySound(yepVoice); uiController.StartDialogues(dialogueList, FRIEND_CHAR, () => { var playerDialogueList = GetDialogueListFromId(new List<EDialogueID> { EDialogueID.FRAPPLIEDMYSELF, EDialogueID.FRIDONTWORK }); uiController.DisplayPlayerDialoguePanel(playerDialogueList); }); } private void PopulateDialogueList() { SDialogue frndDontUWorkAtLettings = new SDialogue(); frndDontUWorkAtLettings.dialogueText = "You work at the ABC Lettings, don’t you?"; frndDontUWorkAtLettings.dialogueId = EDialogueID.FRDONTUWORKATLETTINGS; frndDontUWorkAtLettings.playerStates = new List<PlayerStates> { PlayerStates.NEUTRAL, PlayerStates.FRNDJOBREVEALED }; Dictionary<PlayerStates, Action> frndDontUWorkAtLettingsRespMap = new Dictionary<PlayerStates, Action>(); frndDontUWorkAtLettingsRespMap[PlayerStates.NEUTRAL] = FrndDontUWorkAtLettingsNeutralAction; frndDontUWorkAtLettingsRespMap[PlayerStates.FRNDJOBREVEALED] = FrndDontUWorkAtLettingsJobRvldAction; frndDontUWorkAtLettings.stateToResponseMap = frndDontUWorkAtLettingsRespMap; dialogueList.Add(frndDontUWorkAtLettings); } - The above snippet defines a dialogue "You work at the ABC Lettings, don’t you?" which can be accessed only if the player's state is either NEUTRAL, or FRNDJOBREVEALED i.e. when the friend character has revealed their job to the player.
-
However, the friend's response will differ for both these states. For
the NEUTRAL state, the 'FrndDontUWorkAtLettingsNeutralAction' will be
invoked, which will make display the friend's response as "How do you
know that? Have you been stalking me?"; whereas for the FRNDJOBREVEALED
state, the response will be "Yeah. How about you, are you working
anywhere?" as defined in the 'FrndDontUWorkAtLettingsJobRvldAction'
above.
-
All other dialogues and their responses for various player states were
defined similarly. The Actions invoked for various states can also
update the player state themselves, so when the player approaches the
NPC again, the dialogue choices displayed will also change accordingly.
-
When the player interacts with an NPC, a list of dialogues based on the
player's state is fetched and forwarded to the UI Controller to be
displayed:
public void Interact() { if (!gameController.CanPlayerMoveOrInteract()) { return; } DialogueManager myDialogueMgr = GetComponent<DialogueManager>(); if (myDialogueMgr == null) { return; } NPCMovement myNpcMovement = GetComponent<NPCMovement>(); if (myNpcMovement != null) { myNpcMovement.StopWalking(); } gameController.DisablePlayerMovement(); List<SDialogue> dialogues = myDialogueMgr.GetDialogueListBasedOnState(); uiController.DisplayPlayerDialoguePanel(dialogues); } -
The UI Controller calls the Spawn() function of the player dialogue
panel, which instantiates each dialogue button using a serialized
prefab, and also initialises the button with the dialogue data, so that
the correct Action is invoked when the button is clicked:
public class PlayerDialoguesOuterPanel : MonoBehaviour { public void Spawn(List<SDialogue> dialogues, UIController uIController) { instantiatedPlayerDialogueInnerPanel = Instantiate(playerDialogueInnerPanelPrefab, transform, false); foreach (SDialogue dialogue in dialogues) { GameObject newDialogueButton = Instantiate(dialogueButtonPrefab); newDialogueButton.GetComponent<PlayerDialogueButton>().Init(dialogue, uIController); newDialogueButton.transform.SetParent(instantiatedPlayerDialogueInnerPanel.transform, false); } this.gameObject.SetActive(true); } }public class PlayerDialogueButton : MonoBehaviour { public void Init(SDialogue dialogue, UIController uiController) { this.dialogue = dialogue; this.uiController = uiController; buttonText.text = dialogue.dialogueText; } public void OnButtonClicked() { this.dialogue.dialogueAction.Invoke(); uiController.HidePlayerDialoguePanel(); } } -
The dialogueAction property of each dialogue is assigned a lambda
function by the NPC's Dialogue Manager that invokes the Action based on
the player's state. This could not be done in the PlayerDialogueButton
class in the previous snippet as it is not aware of the player state,
nor should it be since it is just a button and should only be
responsible for button-related behaviour (Single Responsiblity
Principle).
public class Friend_DialogueMgr : DialogueManager { new void Start() { dialogueList = new List<SDialogue>(); PopulateDialogueList(); for (int i = 0; i < dialogueList.Count; i++) { SDialogue currentDialogue = dialogueList[i]; currentDialogue.dialogueAction = () => { InvokePlayerDialogueAction(currentDialogue.stateToResponseMap); }; dialogueList[i] = currentDialogue; } base.Start(); } private void InvokePlayerDialogueAction(Dictionary<PlayerStates, Action> rshipToActionMap) { if (!rshipToActionMap.ContainsKey(stateWPlayer)) { Debug.LogError("ERROR: No action defined for this dialogue at " + stateWPlayer + " state."); return; } rshipToActionMap[stateWPlayer].Invoke(); } }
Dialogue UI Display
- With so much text to feed to the player, the UI system needed to have a common mechanism to display dialogue regardless of its speaker.
-
The speaker also needed to be identified to correctly display their name
and picture on the dialogue panel. An enum and structure were used for
this purpose:
public enum ECharacters { NARRATOR, LEAD, FRIEND, BULLY, JANITOR, MANAGER }[System.Serializable] public struct SSPeakerInfo { public ECharacters speaker; public string speakerName; public Sprite speakerImg; }
-
A single method called 'StartDialogues' was defined which initialised
the speaker UI elements, cycled through the dialogue list and invoked
the provided action at the end:
public class UIController : MonoBehaviour { public void StartDialogues(string[] npcDialogues, ECharacters speaker, Action dialogueEndAction) { gameController.DisablePlayerMovement(); DisplayNPCDialoguePanel(speaker); StartCoroutine(RunThroughNPCDialogues(npcDialogues, dialogueEndAction)); } private void DisplayNPCDialoguePanel(ECharacters speaker) { if (speaker == ECharacters.NARRATOR) { npcDialoguePanel.HideNPCSpeakerDetails(); } else { SSPeakerInfo speakerInfo = speakerToInfoMap[speaker]; npcDialoguePanel.SetNPCSpeakerDetails(speakerInfo.speakerName, speakerInfo.speakerImg); } npcDialoguePanel.Spawn(); } private IEnumerator RunThroughNPCDialogues(string[] npcDialogues, Action dialogueEndAction) { for (int i = 0; i < npcDialogues.Length; i++) { npcDialoguePanel.SetNPCDialogueText(npcDialogues[i]); npcDialogueAdvanced = false; yield return new WaitUntil(() => { return npcDialogueAdvanced; }); } HideNPCDialoguePanel(); if (dialogueEndAction != null) { dialogueEndAction.Invoke(); } } //Called when the dialogue panel is clicked //This causes the 'RunThroughNPCDialogues' to move to the next dialogue public void AdvanceNPCDialogue() { npcDialogueAdvanced = true; } } -
All game characters call this function to display their dialogues,
resulting in a single point to handle UI dialogue display, such as in
the example below:
private void FrndMaybeICanHelpAction() { string[] dialogueList = { "There’s this big bully, Roger, who’s always picking on my sister.", "We’ve complained to the teachers about him but it’s no use.", "Do you think you could get him to stop harassing my sis?" }; stateWPlayer = PlayerStates.FRNDBULLYMENTIONED; uiController.StartDialogues(dialogueList, FRIEND_CHAR, () => { var playerDialogueList = GetDialogueListFromId(new List<EDialogueID> { EDialogueID.FRWILLTALK2BULLY, EDialogueID.FRWONTTALK2BULLY }); uiController.DisplayPlayerDialoguePanel(playerDialogueList); }); } -
The above function is called when the player offers to help a friend.
The 'StartDialogues' function is then called with the friend character
as the speaker which initialises the speaker panel with the name and
image of the friend. After cycling through the three dialogues provided,
the lambda function provided as the dialogueEndAction is called which
displays the dialogue buttons pertaining to the two dialogue IDs -
'FRWILLTALK2BULLY' and 'FRWONTTALK2BULLY.'
Interaction
public void Interact()
{
if (!CanPlayerInteract())
{
//Dont let player interact with safe until player accepts safe crack mission
//or if player has already cracked the safe
return;
}
if (instantiatedMinigame != null)
{
return;
}
instantiatedMinigame = Instantiate(safeCodeMinigame);
instantiatedMinigame.GetComponent<SafeCodePuzzle>().Init(this, audioController, gameController);
gameController.DisablePlayerMovement();
}
void Update()
{
if (objectInteractAction.action.WasReleasedThisFrame() && currentInteractable != null
&& !gameController.IsGamePaused())
{
currentInteractable.Interact();
}
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision != null)
{
IInteractable interactable = collision.gameObject.GetComponent<IInteractable>();
if (interactable != null)
{
currentInteractable = interactable;
currentInteractable.OnPlayerEnteredToInteract();
}
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if (collision != null)
{
IInteractable interactable = collision.gameObject.GetComponent<IInteractable>();
if (interactable != null)
{
interactable.OnPlayerExited();
currentInteractable = null;
}
}
}












