Project Categories
Project Setting | Game Jam |
Team Size | 1 |
Role(s) | Creator / Developer |
Languages | C# |
Software/Tools Used | Unity (engine), Visual Studio (code), Blender (modeling), GIMP (texture editing/generation), git bash/Github Desktop (ver. control), Audacity (audio editing), LMMS (music composition) |
Status | Released |
Time Period | December 2020 - January 2021 |
About
A new housing community part of a larger organization is being built where you, the player, once lived. A nagging feeling called to you to find out the fate of what you used to call home. You discover the organization is developing a VR house touring program decide to look into it to see if it may hold the answer.
Description from the official game page
My New Home - 01 . resi is my entry to the HQ_Residential_House Game Jam hosted by developers corpsepile, Breogán Hackett, and YouTuber John Wolfe. This was a solo submission, where I fully recreated the (in)famous asset HQ Residential House with a certain aesthetic in mind. The game is largely centered around world interaction and item puzzles with interactive dialogue found as you progress.
My Work
Modeling
- Studied and fully re-created the House layout using ProBuilder in Unity
- Created various 3D models in Blender based off pre-determined list of assets seen while studying the House
- Organized models into batched collections for unwrapping and texturing into unified texture atlases in consideration of runtime performance
- Set up models with the intention of making as many have the potential to be used in some way as possible (e.g. cabinets with working cabinet doors, etc.)
- Created character models and ensured they used the same rigs and facial expression setup to make universal animations/expressions
Gameplay
- Fit the "VR house tour simulator" narrative by making as much of the house (barring large furniture) interactable as possible, with creative uses of house objects for advancing the game story
- Most objective-related objects can be added and removed from player inventory
- Certain objects are based on world-interaction, such as a laptop (with a PIN login screen) and a safe (with rotateable knobs)
- Some objects have actions that are based on whether you have something in your inventory or not, such as a laptop (requires power cable), dining cabinet doors (requires a key) and a stuck note (requires a screwdriver to wedge it from between a TV frame)
- Many containers use an open/closed state system with Unity's Animator system, such as drawers, doors, cabinets, toilet lids, etc.
- Some objects can be inspected, such as books and a stuck note
- Miscellaneous objects with grabbing mechanics applied are deemed 'flushable' and can be flushed down toilets
- Placed objective-related items around the House in such a way that every room of the house is used
- Created scripted events such as trigger-based scares, music track changes, and dialogue output around the House and on certain item pickups
UX/UI
- Created various UI sprites for dialogue emotion particles and UI elements which fit the aesthetic of the game
- Created a dialogue system with skippable single-character text dumping, defined emotional states for character model animation/facial expressions, and occasional dialogue player response options with character reactions
- Utilized Unity's Input System to allow for multiple input methods and gamepad cursor movement/actions for various UI elements
- Designed a level select menu for teleporting between rooms for when the doors are blocked by the narrative antagonist
- Created an inventory overlay with a context menu that can be open for items in the inventory to allow for removing items, getting hints, or combining them with other items (assuming requisites are also in inventory); inventory provides a warning if it is full when trying to pick up something else
- Provided options for the player for text output speed, camera rotation sensitivity, and volume controls for various audio outputs
Audio
- Created a short sound track consisting of two cafe-jazz-inspired tracks to fit house exploration and one dark ambient track to fit the endgame mood
- Recorded and edited various sound effects for the various interactables in the game
- Recorded and edited dialogue shots (sped up clips of individual letters of the alphabet) for the dialogue system
Samples
These are some samples from the project.
Videos
Gameplay trailer
Screenshots

Level select and dialogue screenshot

Inventory and dialogue screenshot

Overlay when looking at a desk drawer

Dialogue with response prompt

Overlay when looking at laptop pin numbers

View of living room
Code
(Note: Comments with "-->
" indicate omitted code under where the comment is for sake of condensing)
DumpDialogue is a coroutine where the core of the dialogue system's UI functionality lives. It is based on several asyncronous loops which output dialogue, responses, etc. in a fashion similar to other role-playing games/etc. which output text per character.
The major loop runs while there is dialogue to output; once it ends, dialogue closes. It starts off updating some internal values as well as calling UpdateEmote (plays emote audio/particles where applicable, setting character animator state (if one exists), and calling EmotionContext.SetEmoteState if applicable (updates eyes)).
It then enters a nested loop of dumping text characters. Inside there, it checks if it should instantly dump text or output a single character; per-character, it will check if it can play a sound clip (typically a high pitch spoken letter) and call EmotionContext.OnCharacterSpoken if applicable (updates mouth), then update its internals and wait for the text speed setting.
When that loop is done, two things can occur, both of which wait for character input - A: if the dialogue requires a player response, create response options and inject dialogue based on response when received B: animate a small arrow which is in-line with some control icons to indicate the player can press a button to skip to the next segment of dialogue.
/// <summary>
/// Handles outputting dialogue audio and text to the dialogue overlay; maintains several internal loops to handle audio, animation, and visual changes
/// </summary>
private IEnumerator DumpDialogue()
{
// Enter coroutine loop while there is still pending dialogue
coroutineRunning = true;
while (pendingDialogue.Count > 0)
{
// 0. sometimes the container gets stuck, ensure it's active
dialogueBox.container.gameObject.SetActive(true);
// 1. update emote audio/animation state
UpdateEmote(pendingDialogue[0].emote);
// 2. get list of characters using string->character array and System.Linq ToList to dump to dialogue box and clear text container
List<char> next = pendingDialogue[0].text.ToCharArray().ToList();
dialogueBox.text.text = "";
// 3. create values for animating pitch to make it sound more organic
float initNextLength = next.Count;
float nextIncrement = 0f;
// 4. enter text dump loop
while (next.Count > 0)
{
// a. if the player hits the action button mid-text, text will instantly dump all pending characters
if (pendingSkip)
// --> omitted block
// b. otherwise, output the next character
else
{
dialogueBox.text.text += next[0];
// if the dialogue audio is not playing, play a dialogue sound; prevents overlapping/cut-off sounds
if (!dialogueBox.audioSource.isPlaying)
{
// system-type only plays a single clip type and doesn't animate pitch
if (dialogueBox.audioType == VoiceType.system)
// --> omitted block
// standard-type plays a clip based on character and Lerps pitch slightly based on the animation curve in inspector
else
{
dialogueBox.audioSource.clip = Dialogue.Instance.GetClipFromCharacter(next[0]);
dialogueBox.audioSource.pitch = Mathf.Lerp(
dialogueBox.audioPitch,
dialogueBox.audioPitch + 0.05f,
dialogueBox.audioPitchCurve.Evaluate(nextIncrement / initNextLength));
}
dialogueBox.audioSource.Play();
// Emotion-Context, which is a container for animating facial expressions (mouth/eyes); OnCharacterSpoken handles animating mouth to form letter sounds; eye blinking is handled in a coroutine from within the EC and eyes are only changed when UpdateEmote is called in step 1
if (currentCharacterEC != null)
currentCharacterEC.OnCharacterSpoken();
}
// --> remove character, increment pitch loop timer by pitch loop factor, and reset to 0 if past the length of loop
// wait for the text speed setting before resuming loop
yield return new WaitForSeconds(dialogueBox.settingTextSpeed);
}
}
// 5a. when text output is done, check if there is to be a player response
DialogueResponse[] responses = Dialogue.GetPlayerResponseOptions(pendingDialogue[0].id);
if (responses?.Length > 0)
{
// --> Update state system DialogueState to awaiting_input, deactivate the arrow-&-controls-indicator to avoid confusing player, and activate response options box
// create options
for (int i = 0; i < responses.Length; i++)
{
// --> Instantiate option template, set parent and position to container space
// --> Get DialogueOption from template (container which represents UI elements of the option), set its text and its index (id for option)
option.Button.onClick.AddListener(() =>
{
pendingPlayerResponse = option.Index;
HQAudioShots.Instance.PlayClip(AudioShot.click);
});
}
// --> wait for player to input response with End of Frame loop after setting pending response to -1; when loop exited (response given), add a deltaTime*5 pause so character's response isn't instant
// remove old dialogue like in non-response loop, but also inject new character responses in front of remaining dialogue
pendingDialogue.RemoveAt(0);
for (int i = responses[pendingPlayerResponse].characterResponse.Length - 1; i >= 0; i--)
pendingDialogue.Insert(0, Dialogue.GetDialogue(responses[pendingPlayerResponse].characterResponse[i]));
// --> clear options, re-activate indicator, de-activate options box, set DialogueState back to open instead of awaiting_input
}
// 5b. otherwise, enter an animation loop to indicate awaiting player input
else
{
// this is separate from 5a to ensure 5a's character responses are added after removal
pendingDialogue.RemoveAt(0);
while (!pendingSkip)
{
// enter a sub-loop to start animating the arrow indicator for skipping text
float t = 0;
float animLength = 1.0f;
Vector2 arrowPositionOnHigh = new Vector2(0, 16);
while (t <= animLength)
{
// once pendingSkip is set to true, break from the while loop
if (pendingSkip)
break;
// until pendingSkip is set to true, animate the alpha and position of the arrow indicator
Color col = dialogueBox.indicatorArrow.color;
col.a = Mathf.Lerp(0.2f, 1.0f, dialogueBox.indicatorAnimationCurve.Evaluate(t / animLength));
dialogueBox.indicatorArrow.color = col;
dialogueBox.indicatorArrow.rectTransform.anchoredPosition = Vector2.Lerp(
arrowPositionOnHigh,
Vector2.zero,
dialogueBox.indicatorAnimationCurve.Evaluate(t / animLength));
t += Time.deltaTime;
yield return null;
}
yield return null;
}
// --> reset arrow indicator once loop is exited
}
pendingSkip = false;
}
// --> indicate coroutine is done and close dialogue panel
}
Places
Game Page (Download latest build, source code, and audio/graphical (model/texture) assets) | Itch.io |