Rijksmuseum Familiespel is a game and tour through the museum where the player needs to scan and find objects on paintings. He/she can do that by using a device with a camera and pointing it at certain objects on the painting. We, in a team of 5, have focused us on a single scenario where the player needs to gather ingredients so the cook can make a nice dinner for a noble family. For this project we utilized the Vuforia Augmented Reality API in combination with the Unity game engine. This was a schoolproject that took us 5 months and we made it in cooperation with our client Rijksmuseum.
I have done mainly technical tasks during this project:
- Made a modular dialogue system with scriptable objectsz
- Implemented and managed the Vuforia platform and targets
- System for triggering quests based on distance and detail level of the painting
- Made a standalone scavenger hunt app so people could playtest this type of game from home (as solution during the pandemic)
- Big influence during concept and ideation process
Scavenger hunt experimental application
Dialogue system code
The dialogue controller makes sure that the dialogue gets displayed based on the players input.
using UnityEngine;
public class DialogueController : MonoBehaviour {
public Conversation[] conversations;
public GameObject speakerLeft;
public GameObject speakerRight;
public SpeakerDisplay speakerDisplayLeft;
public SpeakerDisplay speakerDisplayRight;
public int activeLineIndex = 0;
public int conversationIndex = 0;
public int currentConvo;
public bool convoActive;
private bool isSkipping;
private static DialogueController instance;
public static DialogueController Instance { get { return instance; } }
private void Awake() {
if (instance != null && instance != this) {
Destroy(this.gameObject);
} else {
instance = this;
}
}
void Start() {
speakerDisplayLeft = speakerLeft.GetComponent<SpeakerDisplay>();
speakerDisplayRight = speakerRight.GetComponent<SpeakerDisplay>();
speakerDisplayLeft.Speaker = conversations[0].speakerLeft;
speakerDisplayRight.Speaker = conversations[0].speakerRight;
isSkipping = false;
}
void Update() {
//Example to show how the function works
if (Input.GetMouseButtonDown(0) && convoActive) {
AdvanceConversation(currentConvo);
}
FastForward();
}
private void FastForward() {
if (Input.GetMouseButton(0) && isSkipping) {
speakerDisplayLeft.delay = 0.01f;
speakerDisplayRight.delay = 0.01f;
}
if (Input.GetMouseButtonUp(0)) {
if (speakerDisplayLeft.isTalking) {
speakerDisplayLeft.delay = conversations[conversationIndex].lines[activeLineIndex - 1].typeDelay;
isSkipping = true;
}
if (speakerDisplayRight.isTalking) {
speakerDisplayRight.delay = conversations[conversationIndex].lines[activeLineIndex - 1].typeDelay;
isSkipping = true;
}
}
}
public void AdvanceConversation(int index) {
currentConvo = index;
if (conversations.Length == 0)
{
return;
}
if (index < (conversations.Length) && index >= 0) {
conversationIndex = index;
speakerDisplayLeft.Speaker = conversations[index].speakerLeft;
speakerDisplayRight.Speaker = conversations[index].speakerRight;
} else {
convoActive = false;
return;
}
if (activeLineIndex < conversations[conversationIndex].lines.Length) {
if (speakerDisplayLeft.isTalking || speakerDisplayRight.isTalking) return;
DisplayLine();
} else if (!speakerDisplayLeft.isTalking && !speakerDisplayRight.isTalking) {
speakerDisplayLeft.Hide();
speakerDisplayRight.Hide();
activeLineIndex = 0;
convoActive = false;
}
}
private void DisplayLine() {
Line line = conversations[conversationIndex].lines[activeLineIndex];
Character character = line.character;
isSkipping = false;
if (speakerDisplayLeft.SpeakerIs(character)) {
if (!speakerDisplayLeft.isTalking) {
speakerDisplayLeft.delay = conversations[conversationIndex].lines[activeLineIndex].typeDelay;
activeLineIndex++;
SetDialogue(speakerDisplayLeft, speakerDisplayRight, line.text);
}
} else {
if (!speakerDisplayRight.isTalking) {
speakerDisplayRight.delay = conversations[conversationIndex].lines[activeLineIndex].typeDelay;
activeLineIndex++;
SetDialogue(speakerDisplayRight, speakerDisplayLeft, line.text);
}
}
}
void SetDialogue(SpeakerDisplay activeSpeakerDisplay, SpeakerDisplay inactiveSpeakerDisplay, string text) {
activeSpeakerDisplay.fullText = text;
activeSpeakerDisplay.Show();
inactiveSpeakerDisplay.Hide();
}
}
This script contains a struct called Line, holding all the information for a single dialogue line. Conversation is a Scriptable Object class that can organize lines in an array that can be tweaked from the Unity inspector.
using UnityEngine;
using UnityEngine.UI;
[System.Serializable]
public struct Line {
public Character character;
public float typeDelay;
[TextArea(2, 5)]
public string text;
}
[CreateAssetMenu(fileName = "New Conversation", menuName = "Dialogue/Conversation")]
public class Conversation : ScriptableObject {
public Character speakerLeft;
public Character speakerRight;
public Line[] lines;
}
Character is another Scriptable Object which holds all the feedback a character delivers to the player during a conversation.
using UnityEngine;
[CreateAssetMenu(fileName = "New Character", menuName = "Dialogue/Character")]
public class Character : ScriptableObject {
public string fullName;
[Header("Sprite expressions")]
public Sprite defaultExpression;
public Sprite[] additionalExpressions;
[Header("Sounds")]
public AudioClip defaultSfx;
public AudioClip[] additionalSfx;
[Header("Popup sprites")]
public Sprite[] popupSprites;
}
This script actually swaps sprites and plays sound effects for the characters based on the data in the conversation lines.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class SpeakerDisplay : MonoBehaviour {
public Image characterImage;
public Image popupImage;
public Text fullName;
public Text textBox;
public float delay;
public bool isTalking = true;
public string fullText;
public string parsedFullText;
public AudioSource audioSource;
private string currentText;
private Character speaker;
private Coroutine typingText;
public Character Speaker {
get { return speaker; }
set {
speaker = value;
characterImage.sprite = speaker.defaultExpression;
fullName.text = speaker.fullName;
}
}
public bool HasSpeaker() {
return speaker != null;
}
public bool SpeakerIs(Character character) {
return speaker == character;
}
public void Show() {
if (typingText != null) StopCoroutine(typingText);
gameObject.SetActive(true);
isTalking = true;
parsedFullText = fullText;
typingText = StartCoroutine(TypeText());
}
public void Hide() {
gameObject.SetActive(false);
popupImage.gameObject.SetActive(false);
textBox.text = "";
}
public void SkipToEnd() {
if (typingText != null) StopCoroutine(typingText);
textBox.text = fullText;
isTalking = false;
}
//The typing of the text, based on a string
IEnumerator TypeText() {
characterImage.sprite = speaker.defaultExpression;
audioSource.clip = speaker.defaultSfx;
if (!audioSource.isPlaying)
audioSource.Play();
for (int i = 0; i < parsedFullText.Length; i++) {
if (parsedFullText[i] == '*') {
audioSource.Stop();
if (parsedFullText[i + 1] == 'p') {
int popupImageIndex = int.Parse(parsedFullText[i + 2].ToString());
Debug.Log(popupImageIndex);
if (popupImageIndex < speaker.popupSprites.Length) {
popupImage.gameObject.SetActive(true);
popupImage.sprite = speaker.popupSprites[popupImageIndex];
}
} else if (parsedFullText[i + 1] == 'd') {
if (speaker.defaultSfx != null)
audioSource.clip = speaker.defaultSfx;
characterImage.sprite = speaker.defaultExpression;
} else {
int index = int.Parse(parsedFullText[i + 1].ToString());
Debug.Log(speaker.additionalExpressions.Length);
if (index < speaker.additionalExpressions.Length) {
Debug.Log("Sprite switch");
characterImage.sprite = speaker.additionalExpressions[index];
}
if (index < speaker.additionalSfx.Length)
audioSource.clip = speaker.additionalSfx[index];
}
if (!audioSource.isPlaying)
audioSource.Play();
parsedFullText = parsedFullText.Remove(i, 3);
}
currentText = parsedFullText.Substring(0, i+1);
textBox.text = currentText;
if (parsedFullText[i] == ' ') {
yield return new WaitForSeconds(0f);
}
yield return new WaitForSeconds(delay);
}
isTalking = false;
}
}