Improving Unity + Ink skills by working on a visual novel prototype
DomesticSeagull GameDev DK30 Quarantine 2020 6 8
Description
My 30 day goal is to have a functional gameplay loop where dialogue can be selected. Sprite art, positioning, interiority and possibly timing should be controllable from the Ink scripting language. I have placeholder art assets commissioned from an illustrator (nmoonart) that I can use, and prior Unity+Ink integration prototypes that can be cleaned up and adapted for this purpose. I’ll try and check in here every two days.
Recent Updates
My computer died! The past two days have been spent sourcing a new SSD and setting up Unity and other necessary software again. Thankfully my version control was up to date, so no data was lost.
I’ve been working on refactoring my display and parser controllers to work with NPC thoughts, and fully integrate them with Slayout. Nothing fancy to show or talk about- just foundational work :)
Week 1 Overview
I started a little early so I got more done than I thought I would - this is more of a Week 1 + prep overview.
I organized my placeholder assets, wrote a preliminary Ink script that demonstrated most of the features that I would want to implement in this prototype, and generated a few rough sketches to get an idea of how I might want the initial UI to look like.
I started getting a head start on the Week 2 goals: I mocked up the sketch in Unity and even made it interactive, mapped out the flow of logic (thought not exhaustively, and not with all edge cases considered), and began work on refactoring.
Today I’m going to spend some time on the last of my Week 2 goals - continuing my refactor, and re-evaluating my plans based on how the project has been going so far.
Today, we’re taking a bit of a detour to think about timed choices. We’ll think about our edge cases and component flows tomorrow!
I’m convinced with only a little extra scripting, I can support timed choices, which will be the trickiest part in implementing timed conversations.
Timed choices allows us to represent some interesting stuff in games, especially ones that have voice acting:
- We get to interrupt NPCs in the middle of their speeches.
- If they’re going off on tangents, we can interrupt or speak over them to make a point.
- But staying silent is also equally making a point - that we’re interested in listening to what they have to say.
- And as a result it feels right that other NPCs “get” to interrupt us (of course, the NPCs aren’t making a real choice here - the interruption is written into the dialogue)
In a text based game with no voice acting, this changes a little bit, but we’ll go into this later.
Implementing timed choices (with dialogue) in Ink will probably go a little like this:
- When choices are available, there should be two ways to exit: making a choice, or taking a timeout choice.
- The timeout choice should be denoted as a choice with special syntax and an amount of time - e.g.
+$ 10secs
where $ is the special character and 10 seconds is the amount of time to wait before timing out. - A separate choice controller should listen for the player making a choice, and also start a timer (if there’s a timed choice).
- If the parser detects a timeout choice, it should send a message to the choice controller to let it know that there’s a timeout choice and it should start a countdown.
- The story controller will continue delivering the story as normal, unless it receives a signal from the choice controller indicating that a choice has been made.
In a non-voice acted game, there’ll probably be no timer. Instead, taking the special “silent” choice will be represented by clicking forward through the dialogue. However, the choice to interject will hang on the top of the screen and won’t get cleared as it normally would: if a silent choice is available, the component in charge of stripping down and clearing the choice bubbles will instead leave them up.
If the choice to interrupt is made at any point, the interrupt is queued (along with the integer representing the interrupt that was chosen - this would probably be stored in an engine-side integer) - and that choice is made at the next available decision point.
(Aside: There’s an awkward edge condition here that needs to be considered - we want the interrupt choices to stay up no matter what, UNTIL the last interrupt choice is shown - but implementing forward lookaheads everywhere for this would be silly and dangerous. A potential fix is either calling an external function that signals that the choice should be cleaned up as in normal behaviour, or setting a tag that does the same thing - in this example script, #clearinterrupt
.)
The takeaway from all of this is that implementing timed choices is probably possible within this framework, but involves so many edge conditions (foreseen and unforeseen) that a good testing framework is an essential prerequisite for including this feature.
-> interruptable_conversation
=== interruptable_conversation
-> monologue
= interrupt
+ Interrupt them.
Player: Whoa, don't you think you've talked about cats for long enough?
NPC: Oh, sorry. I got carried away.
->wrapup_knot
+ [$...]
<>
->->
= monologue
// The character can get interrupted at any point in the conversation flow.
// Note that the player character doesn't interrupt themselves.
// When you make an interrupt choice, you're _queueing_ an interrupt.
// This means that the interrupt occurs at the next break point.
// You CAN fragment sentences with interrupts to make interrupts more ugly, and glue them back together afterwards, but this would require work on the engine side.
NPC: I could talk about cats for days.
-> interrupt ->
NPC: They're just so nice.
-> interrupt ->
NPC: The way they're so lazy.
NPC: The way they hate all humans.
-> interrupt ->
// This is the player's last chance to interrupt. If they choose the silent choice here (by staying silent for X seconds, or by continuing forward), the option to interrupt will disappear.
// Basically, in-game, the player's ability to interrupt disappears before the NPC says the next line here.
NPC: The way they just exude arrogance. #clearinterrupt
-> timeout_knot
=timeout_knot
// If the conversation gets to here, the player's run out of time to make an interjection.
// We'll also (usually) continue the conversation here.
NPC: The way they go nuts over food.
NPC: Hmm, maybe I've talked about cats for too long.
-> wrapup_knot
= wrapup_knot
// In this knot, we try and reel all the possible branches of the conversation back in, unless the player's chosen to do something drastic (like shoot their conversation partner).
NPC: Sorry, I just really like cats. I guess you can tell.
The story continues.
-> DONE
Alright - the bit I’ve been dreading - it’s time to plan out how we’re going to refactor this code!
At the moment, Ink-Unity integration is all stuffed into a single horrific 185-line controller that manipulates a bunch of manually-set components and pre-fabs.
This isn’t optimal:
- It’s ridiculously difficult to work out what the flow of logic is right now.
- Responsibility for different components and actions is muddied and shared between a bunch of different functions.
- We have no way of instantiating character sprites on the fly (or at least call matching prefabs)
- There’s a lot of tangled dependencies here - it tries to handle story flow, parsing, UI setup and layout (!!!), and even choices when this should probably be separated as much as possible.
It’s worth mapping out what we think our script currently does:
- It generates an Ink story.
- It grabs a whole load of prefabs to generate the UI.
- A story controller runs the Ink story flow - spitting out content until we hit a choice, and then waiting for that choice to be made. At the moment it also listens for input, but this responsibility shouldn’t be shared.
- If we’ve not hit a choice, we parse for for special commands (like controlling emotes, controlling NPC dialogue, controlling the speaker). Based on the results of this parse, we signal different modules to do different things - change the message, change the character art on-screen, trigger other on-screen/off-screen events, change non-Ink gamestate.
- If we hit a choice, a separate choice controller parses and makes similar decisions, just with the choice UI and offering choices.
- All the secondary modules receive messages from their respective parsers and proceed to do things with those messages, like instantiate character sprites, or change messages, or put up or strip down UI.
Tomorrow we should consider edge cases - places where we’ve forgotten to set character art in the Ink script, super long messages, what happens when we accidentally set too many choices in the script, what happens when we forget to purge NPC interiority. It’s no replacement for test driven development, but I think getting used to TDD in Unity is a task for another DK30 :)
I also want to consider and sketch out a variation on the UI that shows ongoing dialogue more like the cascades in a instant message log.
Here’s a dumb conversation as a reward for getting to the bottom of this text dump!
Today we created some rough placeholders for the speech bubbles.
We hooked up Slayout to set the initial dimensions for the background, dialogue boxes & resolution scaling (we still need to do Slayout hookup for the choice boxes and character art, but that’s a task for another day).
Finally, we hooked everything up together (slapdash) - we’ll have to refactor all of it) with the following script:
Tallow: Uh, this is a bit of a dumb question, but I guess we have time to kill...
Tallow: What's your opinion on memes?
* [They're...okay, I guess.]
Serena: I'm...not a fan of them. They all say like, the same stupid thing, rehashed a million different times.
* [I love them!]
Serena: GOD. I. LOVE. THEM.
Tallow: You...do?
Serena: Yeah! Like, especially the one with the cat? That's eating?
Serena: God, I love that one.
Tallow: I sort of regret asking now...
Here’s a gif of everything in motion!
We still need to implement a third dialogue box (and maybe dynamic scaling) - but this is fine for a mockup!
All I could do today was survive. Tomorrow I’ll just draw some crappy UI placeholders in MS paint :)
Today I spent some time getting acquainted with Slayout, and determining whether it suits my project.
I went through the Slayout github page and manipulated some of its examples.
Reading the Slayout readme.md was informative:
The TLDR is that Slayout is a framework that provides its own layout properties for arranging RectTransforms, that it thinks are more intuitive/useful than the Anchor/Pivoting system that comes with Unity.
I then experimented with the examples that come with Slayout - just manipulating and experimenting with the options offered by the framework.
I didn’t manage to get as much done today as I wanted to - I overexerted myself yesterday juggling a lot of non-project things. That’s okay!
It seems like Slayout is well suited to my project, so I’ll be moving ahead with integrating it into my project tomorrow - just hooking the scripts in and setting a single object with it.
If I have time tomorrow, I’ll also take a closer look at the text animation example using Slayout. I can also look to hook up more UI objects with Slayout, with the goal of having everything’s layout set by the framework, so I can use its animation framework.
###[WIP - working on this update as we go today.]
Today’s Goals
Today we’ll be looking at UI presentation and animation possibilities!
Learning
I’m not very well versed with Unity, so this is a good opportunity to learn a lot of things.
I’ve collected a bunch of resources I can go through on this page over here.
Obviously, I’m not going to have time to go through everything here, so I’ll need to prioritize!
Today I’ll focus on going through this beginner’s tutorial on Unity UI fundamentals by Ray Wenderlich. I won’t be executing any of its examples to save on time - I’ll just be taking notes to try and get an overview of the topic so I’m more familiar with the vocabulary I’ll need to do my own research into Unity’s documentation when that time comes, and if I don’t understand a certain part, I’ll make a note of it and keep going forward. I don’t need to build a robust understanding of everything here - the goal instead is to have a passing knowledge of common Unity UI concepts.
Estimated time: 2-3 hour long sessions (which means I should probably break this task down further as I get into it)
If I have time or energy after this, I’ll also check out this Unity tutorial on setting up UI menus with logical navigation flows. I estimate it’ll take me about forty minutes to get through.
Experimenting with the Slayout Framework
Today I’m also playing with Slayout, a convenient open-source UI animation and layout framework extension for Unity created by Inkle.
Playing with this is going to teach me a lot - how to properly crack open and integrate components into Unity (or, if not properly, at least how to crack them open), and with luck I’ll be able to integrate it into my prototype.
My initial attempts at importing it were sort of bunk! I just tried wanging the slayout project root straight into a test Unity project, which caused a huge amount of global namespace conflicts. Opening the slayout project root resulted in a test project with working Slayout examples that I could then experiment with though.
Estimated time: 2 hours of play, continue tomorrow
Mocking up some placeholder speech bubble sprites
If I have time and/or get distracted, I can spend some time mocking up placeholder speech bubble assets for the prototype! This will need to be done at some point before Sunday anyway, but it’s fine if I can’t get to it today.
Current time budget
- 2-3 hours - Building a passing knowledge of Unity’s UI systems
- 2 hours - Playing with Slayout’s existing examples.
- *Working out how to add Slayout functionality to my existing project.
Stretch
- 40 minutes - building a better UI navigation flow in Unity tutorial.
- 1 hour - building placeholder assets for speech bubbles/thought bubbles.
Total
- 4 - 6 hours
Conclusion
I ended up taking the notes for the UI tutorial at this link.
I think I understand the fundamentals of how to responsively scale a Unity UI rect element a lot better now. I think I still need to play with it a lot more to fully understand it, but I at least have a passing knowledge of rect transform manipulation now.
I unfortunately didn’t get enough time to experiment further with Slayout beyond loading the base projects - I got distracted by other responsibilities. I’ll leave this as a task for tomorrow.
Tomorrow’s goals:
- Apply Slayout to my project.
- Apply some basic text animation using Slayout.
- Start thinking about parsing and dynamic speaker/sprite assignation
Started importing assets and hooking up the test Ink script into a Unity mockup.
It’s somewhat interactive - you can click through text, and you can technically select choices - they’re just not presented very prettily. There’s some boilerplate parser code leftover that already parses the names just fine, but I’ll need to write a new addition to the parser code that also parses NPC interiority.
I’ll also need to hook up the choices (+ fake NPC choices) to relevant UI. Tomorrow I’ll task myself with researching dynamic thought/speech bubble implementation in Unity and mocking up some sprites I can use for testing.
This update’s relatively short - I didn’t have much time today due to needing to work overtime. I spent about ten minutes considering how to dynamically adjust the choice bubbles to account for anywhere between 1 and 8 choices at a time, and variable amounts of text.
Alright - let’s keep going.
Today we worked on a rough sketch detailing the UI for choice selection - both for the player character, and a fake one for NPCs.
The idea here is that we have an Oxenfree-like choice selection for the player - but we also have one for other NPCs who are about to speak. The player can see what the NPCs might have said, and what the NPCs are strongly considering saying.
For example, an NPC might be ready to insult the player - hovering over an argumentative choice. But after the player defuses the situation, they might hover a less aggressive choice and choose to voice that instead.
In other cases, this can be used to represent “Spill words” from NPCs - words left unspoken, but almost said. An NPC that’s too timid to speak out loud might have some interesting spill words…
For now, I just want to focus on implementing the basics of this mechanic - in future, there are fancier mechanical things I could try, like fading or obscuring an NPC’s choices if they feel like the player’s not being listening closely enough to them, or to represent disconnect with that character.
Code folding in VS Code along with Bruno Dias’ Ink for VS Code extension makes the script a little easier to read.
I can probably add some custom markup/conditional formatting later by forking Bruno Dias’ repo if I need it to be any easier to read than this, but I’m happy with this for now.
Working through the prepwork a little early, just to ease into things.
Rough placeholder assets commissioned from the artist (nmoonart) have been organized in an easy to access online folder.
I also wrote a prototype Ink script that fulfills most of my necessary criteria - there’s room for cleaning up the formatting so it’s a little easier to follow, but I’m happy with where it is for now.
* It's not worth it.
Serena: This absolutely isn't worth the trouble it's going to cause. Let's stop this thing here before people get hurt.
@Miyaki Thank god. I thought you were insane.
@+Miyaki I'm glad we're on the same page here.
Miyaki: I'm glad we're on the same page on this one.
Miyaki: Thank you.
Serena: For what?
Miyaki: You know.
Miyaki: For listening.
* Sure, let's give it a go.
Serena: Sure, why not? What's the harm in trying?
@Miyaki I don't think this is a great idea...
@+Miyaki Is this...is this safe?
Miyaki: Is this...is this safe?
** Acceptable risks.
Serena: It's an...acceptable risk.
@Miyaki I am _literally_ an actuary. You could have asked me to sim this.
@+Miyaki I hope you're right...
Miyaki: I really, really hope you're right about this...
** Safe enough.
Serena: I...think it's safe?
Serena: I mean, it's not like we could have simmed this or anything.
@+Miyaki You could have asked me to sim this.
@Miyaki I hope you're right...
Miyaki: I'm...I'm a professional actuary. Simulating things is my job.
Miyaki: You could have asked me to sim this.
@Miyaki: But you didn't.
Serena: Oh.
Estimated Timeframe
Apr 24th - May 24th
Week 1 Goal
Prepwork Organize the placeholder assets your artist has given you.
Write a prototype Ink script that fulfills the following conditions:
- Triple choices.
- Reserved characters for denoting “interiority” for other characters, and “selection”.
Create a rough sketch demonstrating a UI for dialogue selection and “interiority” from other characters. This will not be final - the intention is to address obvious issues before putting more work into this project.
Week 2 Goal
Scaffolding, UI and UX work
In Unity, create a non-interactive mock-up of the UI sketch you developed in Week 1.
Map out what needs to be controlled for a single conversation loop, from dialogue -> choice -> dialogue -> reaction -> dialogue -> new character -> choice. (parsing Ink input, showing dialogue, displaying new art or hiding old art, parsing and showing interiority, timing, what each click does)
Based on this planning, modify the Unity UI/UX mockup and or the Ink script and your plans for Weeks 3 and 4 as necessary.
List & link component responsibilities.
Connect everything to SLayout.
Finish the basic UI.
Week 3 Goal
Integration
Study or adapt one of your previous Unity + Ink integration projects and retain the parts of the code that are relevant to anything you identified in the task above.
Integrate the code in this approximate order:
- Ink input.
- Input parsing.
- Build up and tear down.
- Correctly displaying non-special input.
- Correctly displaying choices.
- Correctly displaying parsed text (names)
- Correctly displaying input assigned to characters.
- Correctly displaying interiority assigned to characters.
- Character/speaker change - allow new characters to enter mid-conversation. Deal with 3 characters on the screen at once.
- Allow for graceful defaults if the script forgets to specify a character entry manually. Allow the script to override these defaults.
- Implement default timing that doesn’t conflict with skip-ahead behaviour.
- Anything else identified in Week 2.
Week 4 Goal
Polish Add the things that make it pop.
- Implement basic entry transitions and animations when a new character is shown.
- Implement basic entry and selection animations for player dialogue and NPC interiority.
Stretch goals:
- Implement “fuzziness” - a bouncy/boiling shader that can be applied to a simple colored silhouette around characters.
- Implement text modification based on parsing.