TappaStory

TappaStory is the last game I made (with five other trainees) during my internship at Mediaheads. Tappastory is a game you can play on mobile or on a tablet. It is a game made for children around the age of three to ten where you can create your own story with many different props like, characters, vehicles, benches, rocks, sign and many more. There is also an option to make speech bubbles, to record your own audio, change the background of the scene, include a short movie or clip in the background and to view your created scene in augmented reality.

My job was to create the speech bubbles and develop an option to record and alter recorded audio. At first the boss of Mediaheads didn’t really know where this app was going to end up, this made developing these options harder. Since it was not clear how many features the speech bubbles were supposed to have I created five different speech bubbles for the player to choose from (a normal speech bubble, a think bubble, a scream bubble, a square bubble with more marked borders and an invisible bubble for displaying text only). The player could resize the bubble to his or her own desire, flip the bubble in direction, alter the colour of the bubble, add text inside the bubble, alter the text colour, change the font, change the font size (with a min and max size) and amend the text alignment. After that, I got told this was too much freedom and I had to delete some of the options for the bubbles. The audio part was more thought out and I knew what the purpose of the audio was going to be, this made it easier to create. I created a record audio option where the player could record brief snippets of audio which he or she could later change the pitch and volume for. When I implemented the speech bubble and audio in the game, I had to develop an option for the player to change the speech bubbles while playing, so the player can correct any spelling mistakes without having to completely recreate the bubble.

This project had its ups and downs. I learned that having a solid idea and plan for developing a game is important because many of my colleagues created to many features for this game which we were told to simplify or completely remove from the game. I also learned how to overcome these setbacks and how to move onward after being told to delete something you worked extremely hard on. In the end, I can look back on a fun and beautiful project, where I learned a lot of new creative ways to work within a canvas and how to save audio files within a game.

TappaStory1

TappaStory2

TappaStory3

TappaStory4

TappaStory5

TappaStory6

TappaStory7

TappaStory8

*These are just some of the scripts I created for this project, I cannot post all of them*

TextBubbleOptions.cs
using System.Collections; using System.Collections.Generic; using UnityEngine.UI; using UnityEngine; namespace TextAndAudioPanels { public class TextBubbleOptions : MonoBehaviour { public GameObject activeBubble; public GameObject createBubblePanel; public Transform bubblePanelTransform; public Transform createBubblePanelTransform; public GameObject[] textBubbleArray; public GameObject[] textBubbleActors; public Font[] fontArray; public Color32[] textColors; public int[] fontSizeArray; private Text speechBubbleText; private Text speechBubblePlaceholder; private InputField speechBubbleInputField; private int fontCount = 0; private int fontSizeCount = 0; private int alignNumber = 1; private int colorCount = 0; private int bubbleCount = 0; // Use this for initialization void Start() { SetBubbleReferences(); } private void SetBubbleReferences() { speechBubbleInputField = activeBubble.GetComponentInChildren(); speechBubbleText = speechBubbleInputField.transform.Find("Text").GetComponent(); speechBubblePlaceholder = speechBubbleInputField.transform.Find("BubbleTextPlaceholder").GetComponent(); speechBubblePlaceholder.fontSize = fontSizeArray[0]; speechBubbleText.fontSize = fontSizeArray[0]; } public void ChangeBubble(int _bubbleIndex) { if (_bubbleIndex < 0 || _bubbleIndex > textBubbleArray.Length - 1) { Debug.LogError("<color=red>Bubble index value out of range! Value (" + _bubbleIndex + ") can be 0-" + (textBubbleArray.Length - 1) + "."); return; } bubbleCount = _bubbleIndex; ChangeBubble(); } public void ChangeBubble() { Color oldColor = Color.black; Font oldFont = null; int oldFontSize = 15; string oldText = ""; bool setReferences = false; if (speechBubbleText != null) { oldColor = speechBubbleText.color; oldFont = speechBubbleText.font; oldFontSize = speechBubbleText.fontSize; oldText = speechBubbleText.text; setReferences = true; } Destroy(activeBubble); GameObject speechBubbleToSpawn = textBubbleArray[bubbleCount]; activeBubble = GameObject.Instantiate(speechBubbleToSpawn, createBubblePanelTransform, false) as GameObject; SetBubbleReferences(); SwitchTextAlignment(false); if (setReferences) { speechBubbleText.color = oldColor; speechBubblePlaceholder.color = oldColor; speechBubbleText.font = oldFont; speechBubblePlaceholder.font = oldFont; speechBubbleText.fontSize = oldFontSize; speechBubblePlaceholder.fontSize = oldFontSize; speechBubbleInputField.text = oldText; } //manually setting the alpha of the new spawned placeholders. because of a bug unity sometimes does or does not use the values of the prefab. speechBubblePlaceholder.color = new Color32(0, 0, 0, 128); } public void ConfirmBubble() { FindObjectOfType().CreateNewTextActor(bubbleCount, activeBubble.GetComponentInChildren().textComponent, fontCount,colorCount,fontSizeCount,alignNumber); CancelButton(); } public void ChangeBubbleForward() { bubbleCount++; if (bubbleCount >= textBubbleArray.Length) { bubbleCount = 0; } ChangeBubble(); } public void ChangeBubbleBackwards() { bubbleCount--; if (bubbleCount < 0) { bubbleCount = textBubbleArray.Length - 1; } ChangeBubble(); } public void SwitchFont(bool setNextFont) { if (setNextFont) { fontCount++; } if (fontCount >= fontArray.Length) { fontCount = 0; } speechBubbleText.font = fontArray[fontCount]; speechBubblePlaceholder.font = fontArray[fontCount]; } public void SwitchTextColor(bool setNextColorCount) { if (setNextColorCount) { colorCount++; } if (colorCount >= textColors.Length) { colorCount = 0; } speechBubbleText.color = textColors[colorCount]; speechBubblePlaceholder.color = new Color32(textColors[colorCount].r, textColors[colorCount].g, textColors[colorCount].b, 128); } public void SwitchFontSize(bool setNextFontSize) { if (setNextFontSize) { fontSizeCount++; } if (fontSizeCount >= fontSizeArray.Length) { fontSizeCount = 0; } speechBubbleText.fontSize = fontSizeArray[fontSizeCount]; speechBubblePlaceholder.fontSize = fontSizeArray[fontSizeCount]; } public void SwitchTextAlignment(bool setNextAlignment) { if (setNextAlignment == true) { alignNumber++; } if (alignNumber > 2) { alignNumber = 0; } switch (alignNumber) { case 0: speechBubbleText.alignment = TextAnchor.MiddleLeft; speechBubblePlaceholder.alignment = TextAnchor.MiddleLeft; break; case 1: speechBubbleText.alignment = TextAnchor.MiddleCenter; speechBubblePlaceholder.alignment = TextAnchor.MiddleCenter; break; case 2: speechBubbleText.alignment = TextAnchor.MiddleRight; speechBubblePlaceholder.alignment = TextAnchor.MiddleRight; break; } } public void CancelButton() { ResetValues(true, true, true, true, true); createBubblePanel.SetActive(false); } /// <summary /// function that is used for reseting specific things i.e. reseting the bubble back to the FIRST bubble, setting the CURRENT bubble back to it's start form, /// setting the text in the bubble back to nothing, setting the text alignment back to center top and resetting the font and size back to the original. /// </summary> /// <param name="resetToStandardBubble"> resetting the bubble back to the original speechbubble left /// <param name="emptyTextInSampleBubble"> emptying the text inside the currently used bubble /// <param name="resetTextAlignment"> setting the text alignment back to middle top /// <param name="resetTextColor"> reseting the text color back to the original black /// <param name="resetFontSizeAndStyle"> setting the font back to its standard one and the size back to 14 private void ResetValues(bool resetToStandardBubble, bool emptyTextInSampleBubble, bool resetTextAlignment, bool resetTextColor, bool resetFontSizeAndStyle) { if (resetToStandardBubble) { bubbleCount = 0; ChangeBubble(); } if (emptyTextInSampleBubble) { speechBubbleInputField.text = ""; } if (resetTextAlignment) { alignNumber = 1; SwitchTextAlignment(false); } if (resetTextColor) { colorCount = 0; SwitchTextColor(false); } if (resetFontSizeAndStyle) { fontSizeCount = 0; SwitchFontSize(false); fontCount = 0; SwitchFont(false); } } } }
TextAndAudioPanels.cs
using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using System.Collections.Generic; using System.Collections; namespace TextAndAudioPanels { public class RecordAudio : MonoBehaviour, IPointerDownHandler, IPointerUpHandler { public int maxTimeToRecord; public GameObject audioPanel; public GameObject audioObject; public GameObject timeSlider; public Image timerRestSlider; public GameObject redCross; public GameObject textTimer; //public bool hasAudioRecording; private AudioOptions audioOptions; private Slider timerSlider; private Text timerText; private bool isRecording; private bool twoDigits; private bool automaticStopRecording; //private bool lerpToOrange; //private bool lerpToRed; private bool recordingSucces; private AudioSource audioSource; private float timeLeft; private float recordedTime; private Color green; private Color orange; private Color red; //temporary audio vector we write to every second while recording is enabled List tempRecording = new List(); //list of recorded clips List<float[]> recordedClips = new List<float[]>(); void Start() { //reference to the audiosource where the final recording should be saved. audioSource = GetComponent(); //setting a reference to the timer slider so it can display the rrecorded time and time left to record. timerSlider = timeSlider.GetComponent(); timerText = textTimer.GetComponent(); timerSlider.maxValue = maxTimeToRecord; timerRestSlider.fillAmount = 0; timeLeft = maxTimeToRecord; audioOptions = audioPanel.GetComponent(); } public void Update() { //turns true if the player is recording audio. if (isRecording) { //if the player has not reached the max time of recording. if (timeLeft > 0) { //the time slider counts down from the max time which is 20 to 0. timeLeft -= Time.deltaTime; timerSlider.value = timeLeft; timerRestSlider.fillAmount = 1 - (timeLeft / maxTimeToRecord); //the digital timer counts up from 0 to th max recording time. recordedTime += Time.deltaTime; //if the recorded seconds is below 10 we want the timer to show an extra 0 infront of the recorded seconds i.e to show 00.09.59 instead of 00.9.59. if (twoDigits == false) { timerText.text = "00.0" + recordedTime.ToString("f2"); } //if the recorded seconds is 10 or above we want the timer to get rid of the extra 0 i.e. to show 00.10.00 istead of 00.010.00. else if (twoDigits == true) { timerText.text = "00." + recordedTime.ToString("f2"); } } //if the timer reaches 0 we want to show the red cross to let the player know the recording has failed. else if (timeLeft <= 0) { redCross.SetActive(true); timerSlider.value = timerSlider.maxValue; timerRestSlider.fillAmount = 0; } if (recordedTime > 10 && recordedTime < 11) { twoDigits = true; } if (recordedTime >= maxTimeToRecord) { timerText.text = "00.20.01"; } if (recordedTime >= maxTimeToRecord - 0.1f && recordedTime <= maxTimeToRecord) { automaticStopRecording = true; timerText.text = "00.20.00"; timerSlider.value = 0; timerRestSlider.fillAmount = maxTimeToRecord; StartRecording(); } } } /// <summary /// this function will add all the smaller recording audio into an array next to all the other recordings. /// </summary> void ResizeRecording() { if (isRecording) { //add the next second of recorded audio to temp vector int length = 44100; float[] clipData = new float[length]; audioSource.clip.GetData(clipData, 0); tempRecording.AddRange(clipData); Invoke("ResizeRecording", 1); } } /// <summary> /// this function will check if the player was or want to start recording and handles accordingly /// </summary> public void StartRecording() { if (Microphone.devices.Length > 0) { //if the player is not recording he now is, or if he was recording he now is not. isRecording = !isRecording; Debug.Log(isRecording == true ? "Is Recording" : "Off"); //if recording is false we want to get the recording of what the player was recording and add it to a object that will hold the recording untill the player decides to record again. if (isRecording == false) { if (recordedTime < 0.10f) { redCross.SetActive(true); timerSlider.value = timerSlider.maxValue; timerRestSlider.fillAmount = 0; //speechBubbleOptions.hasAudioRecording = false; Debug.Log("Recorded audio to short to use, please record longer."); recordingSucces = false; } else if (recordedTime > maxTimeToRecord) { redCross.SetActive(true); //speechBubbleOptions.hasAudioRecording = false; Debug.Log("Recorded audio to long to use, please record shorter clips."); recordingSucces = false; } else { recordingSucces = true; //stop recording, get length, create a new array of samples int length = Microphone.GetPosition(null); Microphone.End(null); float[] clipData = new float[length]; audioSource.clip.GetData(clipData, 0); //create a larger vector that will have enough space to hold our temporary //recording, and the last section of the current recording float[] fullClip = new float[clipData.Length + tempRecording.Count]; for (int i = 0; i < fullClip.Length; i++) { //write data all recorded data to fullCLip vector if (i < tempRecording.Count) fullClip[i] = tempRecording[i]; else fullClip[i] = clipData[i - tempRecording.Count]; } recordedClips.Add(fullClip); audioSource.clip = AudioClip.Create("recorded samples", fullClip.Length, 1, 44100, false); audioSource.clip.SetData(fullClip, 0); //adding the complete recording to a object that will hold this recording untill the player desides to record again or create the audio object in it's scene. AudioSource audioObjectSource = audioObject.GetComponent(); audioObjectSource.clip = audioSource.clip; //GameObject createdAudioObject = GameObject.Instantiate(objectWithRecordedAudio, canvasTransform, false) as GameObject; //AudioSource createdAudioSource = createdAudioObject.GetComponent(); //createdAudioSource.clip = audioSource.clip; } } else { //stop audio playback and start new recording... audioObject.GetComponent().Stop(); tempRecording.Clear(); Microphone.End(null); audioSource.clip = Microphone.Start(null, true, maxTimeToRecord, 44100); //Invoke("ResizeRecording", 1); } } else { Debug.LogWarning("<color=yellow>Warning! No microphone detected. If you want to record audio, make sure to connect a device that can record audio!"); } } public void ResetTimer() { timerText.text = "00.00.00"; } /// <summary> /// gets called when the recording button is pressed to start the recording. /// </summary> /// <param name="eventData"></param> public void OnPointerDown(PointerEventData eventData) { //making sure the timer is using the correct time they have to display. timerSlider.value = maxTimeToRecord; timerRestSlider.fillAmount = 0; timeLeft = maxTimeToRecord; audioOptions.hasAudioRecording = false; //setting the correct recorded time, disabling the twodigits in the timer and setting the correct text for the single digit timer. recordedTime = 0; twoDigits = false; recordingSucces = false; ResetTimer(); //starting the recording. StartRecording(); } /// <summary> /// gets called whenever the player lets go of the recording button. /// </summary> /// <param name="eventData"></param> public void OnPointerUp(PointerEventData eventData) { //disabling the recording and checking if the player stayed within the max recording time to display the checkmark to show the recording finished. if (automaticStopRecording == false) { StartRecording(); } if (recordingSucces == true) { audioOptions.hasAudioRecording = true; //speechBubbleOptions.hasAudioRecording = true; } automaticStopRecording = false; } }
SaveToWav.cs
using System; using System.IO; using UnityEngine; using System.Collections.Generic; namespace TextAndAudioPanels { public static class SaveToWav { const int HEADER_SIZE = 44; public static bool Save(string filename, AudioClip clip) { if (!filename.ToLower().EndsWith(".wav")) { filename += ".wav"; } var filepath = Path.Combine(Application.persistentDataPath, filename); Debug.LogWarning(filepath); // Make sure directory exists if user is saving to sub dir. Directory.CreateDirectory(Path.GetDirectoryName(filepath)); using (var fileStream = CreateEmpty(filepath)) { ConvertAndWrite(fileStream, clip); WriteHeader(fileStream, clip); } return true; } static FileStream CreateEmpty(string filepath) { var fileStream = new FileStream(filepath, FileMode.Create); byte emptyByte = new byte(); for (int i = 0; i < HEADER_SIZE; i++) //preparing the header { fileStream.WriteByte(emptyByte); } return fileStream; } static void ConvertAndWrite(FileStream fileStream, AudioClip clip) { var samples = new float[clip.samples]; clip.GetData(samples, 0); Int16[] intData = new Int16[samples.Length]; //converting in 2 float[] steps to Int16[], //then Int16[] to Byte[] Byte[] bytesData = new Byte[samples.Length * 2]; //bytesData array is twice the size of //dataSource array because a float converted in Int16 is 2 bytes. int rescaleFactor = 32767; //to convert float to Int16 for (int i = 0; i < samples.Length; i++) { intData[i] = (short)(samples[i] * rescaleFactor); Byte[] byteArr = new Byte[2]; byteArr = BitConverter.GetBytes(intData[i]); byteArr.CopyTo(bytesData, i * 2); } fileStream.Write(bytesData, 0, bytesData.Length); } static void WriteHeader(FileStream fileStream, AudioClip clip) { var hz = clip.frequency; var channels = clip.channels; var samples = clip.samples; fileStream.Seek(0, SeekOrigin.Begin); Byte[] riff = System.Text.Encoding.UTF8.GetBytes("RIFF"); fileStream.Write(riff, 0, 4); Byte[] chunkSize = BitConverter.GetBytes(fileStream.Length - 8); fileStream.Write(chunkSize, 0, 4); Byte[] wave = System.Text.Encoding.UTF8.GetBytes("WAVE"); fileStream.Write(wave, 0, 4); Byte[] fmt = System.Text.Encoding.UTF8.GetBytes("fmt "); fileStream.Write(fmt, 0, 4); Byte[] subChunk1 = BitConverter.GetBytes(16); fileStream.Write(subChunk1, 0, 4); UInt16 two = 2; UInt16 one = 1; Byte[] audioFormat = BitConverter.GetBytes(one); fileStream.Write(audioFormat, 0, 2); Byte[] numChannels = BitConverter.GetBytes(channels); fileStream.Write(numChannels, 0, 2); Byte[] sampleRate = BitConverter.GetBytes(hz); fileStream.Write(sampleRate, 0, 4); Byte[] byteRate = BitConverter.GetBytes(hz * channels * 2); // sampleRate * bytesPerSample*number of channels, here 44100*2*2 fileStream.Write(byteRate, 0, 4); UInt16 blockAlign = (ushort)(channels * 2); fileStream.Write(BitConverter.GetBytes(blockAlign), 0, 2); UInt16 bps = 16; Byte[] bitsPerSample = BitConverter.GetBytes(bps); fileStream.Write(bitsPerSample, 0, 2); Byte[] datastring = System.Text.Encoding.UTF8.GetBytes("data"); fileStream.Write(datastring, 0, 4); Byte[] subChunk2 = BitConverter.GetBytes(samples * channels * 2); fileStream.Write(subChunk2, 0, 4); // fileStream.Close(); } } }