Skip to content

Instantly share code, notes, and snippets.

@samsheffield
Created December 5, 2023 23:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save samsheffield/392d4fc628c07e4002238e2fc73315d9 to your computer and use it in GitHub Desktop.
Save samsheffield/392d4fc628c07e4002238e2fc73315d9 to your computer and use it in GitHub Desktop.
Ink + Unity demo files (Narrative Design F23)
using System;
using System.Collections;
using UnityEngine;
public class InkInteraction : MonoBehaviour
{
// This is the ink JSON asset
[SerializeField] private TextAsset inkJSON;
// This is a reference to the Ink Manager script designed to run the story
// Note: You could also make the Ink Manager a singleton and avoid having to create a variable here
[SerializeField] private InkManager inkManager;
// This indicates whether we are able to interact with something and start the Ink story
// Note: In this example you will need to set it in the Inspector to true, but a practical use would be using a trigger collider
[SerializeField] private bool canInteract;
void Update()
{
// Can we interact? Is the story NOT already playing? Is it ready?
if (canInteract && !inkManager.inkIsPlaying && inkManager.isReady)
{
// Start the story with the referenced Ink JSON asset using an input action
if (Input.GetKeyDown(KeyCode.Return))
{
StartCoroutine(StartAfterDelay());
}
}
}
// Need a little cooldown since we are using the same input action for interaction and story progression
private IEnumerator StartAfterDelay()
{
yield return new WaitForSeconds(.2f);
inkManager.StartStory(inkJSON);
}
// Optional: trigger the start of the interaction
private void OnTriggerEnter(Collider other)
{
canInteract = true;
}
private void OnTriggerExit(Collider other)
{
canInteract = false;
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Ink.Runtime;
using TMPro;
using UnityEngine.EventSystems;
using UnityEngine.Serialization;
public class InkManager : MonoBehaviour
{
// This holds our current Ink story
[SerializeField] private Story currentStory;
// This is a reference to the UI panel containing the story test
[SerializeField] private GameObject storyPanel;
// This is a reference to the text used to render the story
[SerializeField] private TextMeshProUGUI storyText;
// This is an array which holds choices from our story
[SerializeField] private GameObject[] choices;
// This is the text used to render the choices
private TextMeshProUGUI[] choicesText;
// This is used to keep track of whether the story is already playing
// Note: It is public because it is being used by the scripts enabling interaction
public bool inkIsPlaying;
public bool isReady = true;
// Create variables for each Ink tag. This is a constant variable so that it can be used with switch()
// Note: Const variables can't be modified at runtime
private const string SPEAKER = "speaker";
void Start()
{
// Disable UI
storyPanel.SetActive(false);
// Set the length of the choices text array to match the maximum number of buttons
choicesText = new TextMeshProUGUI[choices.Length];
// Then get references to the text component of each one
int index = 0;
foreach (GameObject choice in choices)
{
choicesText[index] = choice.GetComponentInChildren<TextMeshProUGUI>();
index++;
}
}
void Update()
{
// Only continue past this point if ink is playing
if (!inkIsPlaying)
{
return;
}
// Use an input action to progress the story
if (Input.GetKeyDown(KeyCode.Return) )
{
ContinueStory();
}
}
// Starts the story and activates the UI
public void StartStory(TextAsset inkJSON)
{
// Set the story to the selected ink JSON file
currentStory = new Story(inkJSON.text);
// The ink story is now playing
inkIsPlaying = true;
// Enable the UI
storyPanel.SetActive(true);
isReady = false;
// Bind the Ink Functions:
// Note about Actions
currentStory.BindExternalFunction("hiUnity", (string hiMessage) => {
Debug.Log(hiMessage);
});
// Begin playing story
ContinueStory();
}
// Progresses the story while possible and signals exit when done
private void ContinueStory()
{
if (currentStory.canContinue)
{
// Set the text to the current block of the ink story
storyText.text = currentStory.Continue();
// If there are choices, display them
DisplayChoices();
// Handle tags
HandleTags(currentStory.currentTags);
}
else
{
ExitStory();
}
}
// Handles what happens when the story is exited
private void ExitStory()
{
// Optional: Reset the story once completed
currentStory.ResetState();
// Unbind any Ink functions
currentStory.UnbindExternalFunction("hiUnity");
// Ink is no longer playing
inkIsPlaying = false;
// Disable the UI
storyPanel.SetActive(false);
StartCoroutine(MakeReady());
}
// Display choices when they are available
private void DisplayChoices()
{
// This is a List which is populated with any current choices
List<Choice> currentChoices = currentStory.currentChoices;
// If there ae more choices in your ink story than buttons that you've set up, this will throw an error
if (currentChoices.Count > choices.Length)
{
Debug.LogError("More choices given than the UI can support");
}
// For each choice detected...
int index = 0;
foreach (Choice choice in currentChoices)
{
// Activate the choice button
choices[index].gameObject.SetActive(true);
// Set the text of the button the choice text in the ink story
choicesText[index].text = choice.text;
index++;
}
// Deactivate any choice buttons not being used
// Note: the count starts at the current value of index started above, not 0
for (int i = index; i < choices.Length; i++)
{
choices[i].gameObject.SetActive(false);
}
// Automatically select the first button so that a player can use input actions to navigate the UI
EventSystem.current.SetSelectedGameObject (choices[0]);
}
// This is used to manage choices
// Note: This is public because it need to be added as a button's OnClick Event
// Note: choiceIndex needs to be set in the OnClick event to match the button order (0,1,2, etc)
public void MakeChoice(int choiceIndex)
{
currentStory.ChooseChoiceIndex(choiceIndex);
}
// Need a little cooldown since we are using the same input action for interaction and story progression
private IEnumerator MakeReady()
{
yield return new WaitForSeconds(.2f);
isReady = true;
}
// Handle all found Ink tags
private void HandleTags(List<string> currentTags)
{
// Parse each set of tags found
foreach (string tag in currentTags)
{
// The tags arrive formatted like this: key:value. This needs to be split into separate key and value strings at the :
string[] splitTag = tag.Split(':');
// Check the split tags to make sure it consists of two parts and throw an error if it doesn't
if(splitTag.Length != 2)
{
Debug.LogError("Tag could not be parsed: " + tag);
}
// Trim can be used to remove any trailing white space
string tagKey = splitTag[0].Trim();
string tagValue = splitTag[1].Trim();
// Do something when a particular tag is found
switch(tagKey)
{
// Add a case for each tag
case SPEAKER:
if(tagValue == "one")
{
Debug.Log("This is speaker one");
}
break;
default:
// Give a warning if an unknown key was found
Debug.LogWarning("An unknown key was found: " + tagKey);
break;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment