Another semester another big project.
This time about a turn-based RPG called Unbound and it's for our Game Production Studio module. This is a group project, and I am the project coordinator and producer of the game. I also help out with the coding and setting up of the UI.
We are 5 on the team
Everyone's roles are listed further below!
Expect a playable build of the game on Itch.io every Friday!
ITCH.IO LINK AT THE BOTTOM OF THE PAGE
Embark on a fantastical journey through a war-torn Kingdom. Explore a mystical world filled with powerful elemental gems that hold the key to unlocking untold power.
As a skilled warrior, you must unbind the elemental gems and harness their power to defeat the forces of evil! Many mystical creatures stand in your way, and by combining the elements you must power through these harsh encounters.
Experience the action-packed battles and quest-filled adventures in stunning 2.5D graphics. Get ready to wield the power of the elements and become a legend in this epic fantasy world.
More details in the Game Concept Powerpoint below!
As the project coordinator, my job is to make sure everyone is staying on track with their tasks and to keep everyone updated on the project status. also I have to make sure I know what everyone is doing and how they do it, offering suggestions on how the artists should use the same art techniques, making sure the game designer stays consistent, and making sure the coder follows naming conventions and keeps the code easy to read and understanding this requires knowledge in all fields.
Im also in charge of the UI for the game and a secondary developer. making UI is an important role as it is one of the main things the players interact with and use in the game.
I'm also in charge of making the sounds for the game, not to be confused with the music, I only concentrate on the world and UI sounds of the game.
Each week we have meetings and send in what we have done, this is important, not only for each other but also so I can see what we need to add to the build scene for that week's build, communication is key.
set up the presentation and delegated the team members to do slides that related to their roles.
Set up meetings with the team to ensure clear communication.
Set up the Miro to ensure the team could keep track of tasks.
Make a dialogue system that can easily be modified.
Made the main menu and graphic settings.
Building and creating each build (Play it here!)
converted everything to the new input system.
Make and maintain the Game Manager.
Making the JSON save and load system.
Create the inventory, status and skills menu.
Decision to cut much of the game for time reasons (Feature Creep)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using UnityEngine.UI;
using UnityEngine.InputSystem;
public class DialogManager : MonoBehaviour
{
#region dialog Input
[Header("Put in dialog in order")]
public List<string> dialogTexts;
[Header("match names with dialog order")]
public List<string> personSpeaking;
#endregion
#region text variables
[Header("Variables for text output")]
public float timeBetweenLetters = .2f;
public float dialogTextSize = 36;
public float timeBetweenDialogs = 1;
public float characterNameSize = 40;
TMP_Text dialogTextBox;
TMP_Text characterName;
string currentText;
#endregion
#region image settings for player
[Header("Player image Settings")]
public Texture PlayerImage;
GameObject characterSprite;
public Vector3 PlayerImagePos;
#endregion
#region Images and names for NPC
public List<Texture> NPCImages;
[Header("In order of sprites above")]
public List<string> NPCNames;
public Vector3 NPCImagePos;
public float imageSpacing;
[Tooltip("If NPC is not an Enemy, Leave blank!")]
public int NPCType;
[Header("DO NOT Change")]
[SerializeField] GameObject ImagePrefab;
List<GameObject> NPCSprites = new List<GameObject>();
bool canInteract = false;
#endregion
#region global image settings
[Range(0f, 1f)]
public float imageAlphaValue;
public float imageAlphaTime;
public float scaleImage;
#endregion
#region Logic for text apperaing
GameObject dialogItems;
private IEnumerator coroutine;
int index;
bool coRunning = false;
bool skip = false;
#endregion
#region input system
public InputActionAsset gameplayActions;
InputAction interact;
#endregion
#region combat info variables
[Header("go to combat"), Tooltip("leave null if non aplicable")]
public int combatSerialNumber;
CombatInfo combatInfo;
GameManager GM;
#endregion
#region setup vars
bool setup = false;
#endregion
#region TEMP
[Header("TEMP")]
public string mainCharacterName;
#endregion
#region awake and update function
//awake function
void Awake()
{
dialogItems = GameObject.Find("DialogItems");
Transform dialogItemsT = dialogItems.transform;
//Dialog box
dialogTextBox = dialogItemsT.Find("DialogText").GetComponent<TMP_Text>();
dialogTextBox.fontSize = dialogTextSize;
//Character names
characterName = dialogItemsT.Find("CharacterNames").GetComponent<TMP_Text>();
characterName.fontSize = characterNameSize;
//new input system setup
interact = gameplayActions.FindAction("Interact");
//combat info
combatInfo = GameObject.FindObjectOfType<CombatInfo>();
GM = GameObject.FindObjectOfType<GameManager>();
}
//update function
void Update(){
//check if player is pressing interact key and within vicinity
if (interact.WasPressedThisFrame() && interact.IsPressed() && canInteract){
if(!setup)
Setup();
GameObject.FindObjectOfType<PlayerMovement>().CanMove(false);
InteracteDialog();
}
}
#endregion
#region setup & takedown Canvas
void Setup(){
Transform dialogItemsT = dialogItems.transform;
//character sprite
characterSprite = Instantiate(ImagePrefab, dialogItemsT, false);
characterSprite.GetComponent<RawImage>().texture = PlayerImage;
characterSprite.transform.localPosition = PlayerImagePos;
//NPC Character sprites
AssignNPCImages(dialogItemsT);
setup = true;
}
void Takedown(){
Destroy(characterSprite);
foreach(GameObject GM in NPCSprites){
Destroy(GM);
}
NPCSprites.Clear();
setup = false;
}
#endregion
#region image and sprite fucntions
//asign images to npc dialog
void AssignNPCImages(Transform DIT)
{
GameObject GM;
for (int i = 0; i < NPCNames.Count; i++)
{
GM = Instantiate(ImagePrefab, DIT, false);
GM.GetComponent<RawImage>().texture = NPCImages[i];
GM.name = NPCNames[i];
GM.transform.localPosition = NPCImagePos + new Vector3(i*imageSpacing*100, 0, 0);
NPCSprites.Add(GM);
}
}
//fade the image function
void ImageFade(RawImage img, float Value = 1)
{
img.CrossFadeAlpha(Value, imageAlphaTime, false);
}
//fade the sprite depending on the person speaking
private void CharacterImageColour(string character)
{
float SI = scaleImage;
if (character == mainCharacterName)
{
for(int i = 0; i < NPCSprites.Count; i++)
{
ImageFade(NPCSprites[i].GetComponent<RawImage>(), imageAlphaValue);
NPCSprites[i].transform.localScale = new Vector3(1, 1, 1);
}
ImageFade(characterSprite.GetComponent<RawImage>());
characterSprite.transform.localScale = new Vector3(SI, SI, SI);
return;
}else if(NPCSprites.Count == 0){
return;
}
for (int i = 0; i < NPCSprites.Count; i++)
{
if (NPCSprites[i].name == character)
{
ImageFade(NPCSprites[i].GetComponent<RawImage>());
Transform NPCST = NPCSprites[i].transform;
NPCST.localScale = new Vector3(SI, SI, SI);
}
else
{
ImageFade(NPCSprites[i].GetComponent<RawImage>(), imageAlphaValue);
NPCSprites[i].transform.localScale = new Vector3(1, 1, 1);
}
}
ImageFade(characterSprite.GetComponent<RawImage>(), imageAlphaValue);
characterSprite.transform.localScale = new Vector3(1, 1, 1);
}
#endregion
#region text and dialog functions
//start dialog / end dialog / skip dialog logic
public void InteracteDialog()
{
//reset dialog
if (coRunning)
{
skip = true;
return;
}
else if (index == dialogTexts.Count)
{
StopAllCoroutines();
index = 0;
Takedown();
dialogItems.SetActive(false);
GameObject.FindObjectOfType<PlayerMovement>().CanMove(true);
Invoke("StartCombat", 1f);
return;
}
//set active and start coroutine
dialogItems.SetActive(true);
coroutine = ShowText(index);
GM.SaveGame();
StartCoroutine(coroutine);
index++;
}
//text code and ui logic
IEnumerator ShowText(int l)
{
coRunning = true;
string text = dialogTexts[l];
//set character name and image
characterName.text = personSpeaking[l];
CharacterImageColour(personSpeaking[l]);
//bring letters onto the screen
for (int i = 0; i < text.Length + 1; i++)
{
currentText = text.Substring(0, i);
dialogTextBox.text = currentText;
if (!skip)
{
yield return new WaitForSeconds(timeBetweenLetters);
}
else
{
yield return new WaitForSeconds(0);
}
}
skip = false;
coRunning = false;
}
#endregion
#region start combat sequence
void StartCombat(){
combatInfo.StartCombat(combatSerialNumber);
}
#endregion
#region trigger enter / exit
//enter / exit trigger
private void OnTriggerEnter(Collider other)
{
print("in");
if (other.CompareTag("Player"))
{
canInteract = true;
}
}
private void OnTriggerExit(Collider other)
{
if (other.CompareTag("Player"))
{
canInteract = false;
}
}
#endregion
}
In order to make the dialogue system easy to use, I made sure that each part of it was modifiable, from the number of characters on screen to the size of the font used.
This was very important as I didn't know what the Game or Level designer wanted to do with every interaction, so making it modifiable on the go without changing code means that the team can easily and quickly throw up dialogue into the scene without much of an issue.
As the game progressed so has the dialogue system, ita now what is used to move the player from the main world to the combat scene.
using SaveLoadSystem;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;
public class GameManager : MonoBehaviour
{
#region player settings
public string MainCharacterName = "";
public int PlayerHealth;
#endregion
#region menu variables
GameObject pauseMenu;
GameObject settings;
public bool menuActive = false;
int menuLayer;
#endregion
#region save / load variables
PlayerSaveData playerSaveData;
SaveOBJ[] OBJList;
#endregion
#region player menu variables
GameObject playerMenu;
GameObject inventoryMenu;
GameObject skillsMenu;
GameObject statusMenu;
public bool playerMenuActive = false;
int playerMenuLayer;
#endregion
#region start function
void Start()
{
// get gameobjects that need to be saved
OBJList = FindObjectsOfType(typeof(SaveOBJ)) as SaveOBJ[];
// get player save data
playerSaveData = FindObjectOfType<PlayerSaveData>();
// find pause & settings menu
pauseMenu = GameObject.Find("PauseMenu");
settings = GameObject.Find("SettingsMenu");
// find player menu
playerMenu = GameObject.Find("PlayerMenu");
inventoryMenu = playerMenu.transform.Find("InventoryMenu").gameObject;
skillsMenu = playerMenu.transform.Find("SkillsMenu").gameObject;
statusMenu = playerMenu.transform.Find("StatusMenu").gameObject;
// reset layers
MenuChangeLayer(0);
PlayerMenuChangeLayer(0);
// set player variables
PlayerPrefs.SetString("PlayerName", MainCharacterName);
PlayerPrefs.SetInt("PlayerHealth", PlayerHealth);
Invoke("DisableDialog", .5f);
LoadGame();
if(PlayerPrefs.GetFloat("Defeated",0) == 1){
GameObject.Find(PlayerPrefs.GetString("enemyName0")).GetComponent<SaveOBJ>().Defeated();
SaveGame();
PlayerPrefs.SetFloat("Defeated",0);
}
LoadGame();
}
#endregion
#region menu Functions
// change layer of menu
public void MenuChangeLayer(int Layer)
{
menuLayer = Layer;
MenuSetLayer(menuLayer);
}
// open / change the menu layer
public void OpenPauseMenu(InputAction.CallbackContext context)
{
if (context.performed)
{
//print("fire");
if (menuActive)
MenuChangeLayer(menuLayer - 1);
else
MenuChangeLayer(1);
}
}
// set the layer
void MenuSetLayer(int layer)
{
switch (layer)
{
case 1:
menuActive = true;
pauseMenu.SetActive(true);
settings.SetActive(false);
Time.timeScale = 0;
break;
case 2:
settings.SetActive(true);
pauseMenu.SetActive(false);
break;
default:
menuActive = false;
pauseMenu.SetActive(false);
settings.SetActive(false);
Time.timeScale = 1;
break;
}
}
#endregion
#region load scene
// load scene
public void LoadScene(string scene)
{
if(scene == null)
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
else
{
SceneManager.LoadScene(scene);
}
}
#endregion
#region save and load system
// save the game
public void SaveGame(){
playerSaveData.SavePlayerData();
foreach(SaveOBJ obj in OBJList){
obj.SaveData();
}
SaveGameManager.SaveGame();
}
// load the game
public void LoadGame(){
if(!SaveGameManager.LoadGame())
return;
playerSaveData.LoadPlayerData(SaveGameManager.CurrentSaveData.PlayerData);
foreach(SaveOBJ obj in OBJList){
LoadOBJ(obj);
}
}
// match the data to the obj and load
void LoadOBJ(SaveOBJ obj){
foreach(OBJData data in SaveGameManager.CurrentSaveData.OBJDataList){
if(obj.gameObject.name == data.name){
obj.LoadData(data);
//print(obj.name);
}
}
}
#endregion
#region disable dialog
void DisableDialog(){
GameObject.Find("DialogItems").SetActive(false);
}
#endregion
#region player menu system
// change layer of menu
public void PlayerMenuChangeLayer(int Layer)
{
playerMenuLayer = Layer;
PlayerMenuSetLayer(playerMenuLayer);
}
// open / change the menu layer
public void PlayerMenuOpen(InputAction.CallbackContext context)
{
if (context.performed)
{
if (playerMenuActive)
PlayerMenuChangeLayer(0);
else
PlayerMenuChangeLayer(1);
}
}
// set the layer
void PlayerMenuSetLayer(int layer)
{
switch (layer)
{
case 1:
playerMenuActive = true;
playerMenu.SetActive(true);
inventoryMenu.SetActive(true);
statusMenu.SetActive(false);
skillsMenu.SetActive(false);
Time.timeScale = 0;
break;
case 2:
inventoryMenu.SetActive(false);
statusMenu.SetActive(true);
skillsMenu.SetActive(false);
break;
case 3:
inventoryMenu.SetActive(false);
statusMenu.SetActive(false);
skillsMenu.SetActive(true);
break;
default:
playerMenuActive = false;
playerMenu.SetActive(false);
inventoryMenu.SetActive(false);
statusMenu.SetActive(false);
skillsMenu.SetActive(false);
Time.timeScale = 1;
break;
}
}
#endregion
}
The Game Manager is the heart of the game, its where all the systems get together and get their information on the status of the game and what the player is up to. making this easy to read is vital as if our lead programmer needs to modify or add on to the system he needs to be able to read all the code and understand it.
Also i made sure that some of the parameters are modifiable from the editor in order to allow the game designer to play around with some values
In the final section of the slice, there are a number of statues in order to change the pace of the game and allow the player to think differently. I implemented the code for these puzzles.
the puzzles work by different coloured gems being picked up and inserted into a specific statue, the exception being one statue with the wrong gem inserted.
Unfortunately, I was under a time constraint while making these, so they proved to be a challenge to get working bug-free for one of our playtests. I decided to use player prefs to keep track of the gems the player collected. the statue would check if the player has acquired the gem and open a gate if it has, then change the sprite to indicate to the player that it has worked.
Each Friday we take our work up and showcase it to the world via our Itch.io page! this requires a day or two of work each time, as it involves pulling the latest build from git, and asking what changes everyone made and what state those changes are in, if they are in a good state then I extract them from their scene and put them into the final build scene. of course, there are always bugs so I spend a lot of time bug fixing and making sure the game doesn't have game-breaking bugs that could ruin playtests.
This had its challenges tho, for example when someone implemented art or code into their scene and didn't fully explain it in Miro or Discord I would be left trying to figure it out on my own, of course, I asked for help when needed to speed up the process.
another challenge I'm facing is people pushing to the wrong scene, sometimes people pushed to the prototype scene or the final build scene without telling me, causing the odd overwrite, nothing major as of yet, but it's something always on the back of my mind. when this happens it calls for a tough call to be made, I need to contact the last person that made a push and ask them to transfer their changes to their own scene and push again after I'll delete the affected scene in GitHub and pull again, the n resume like before.
Being the producer and managing the project had interesting problems and challenges.
I'm not known to be the finest communicator, but I would like to consider myself good at delegating tasks evenly, but unfortunately, it wasn't a smooth ride.
each week we are meant to be having a meeting but immediately we noticed this wasn't going to be easy. most of us have a job on the side, and again all of us have many projects going on, all of this combined with other teams wanting weekly updates means we don't have a lot of time left for meetings.
but when we found the time we made it count with productive meetings.
one of my flaws was not being assertive enough with deadlines, I managed to catch it before it was too late, but for example, our programmer didn't finish his task after 4 weeks, this, unfortunately, caused knock-on effects for the game, but since that mistake, I've made it clear that times wasn't something we had, so I've been more on top of my team for deadlines as the project comes to a close. as of writing these we are all done except for me and the programmer, it did when I've built the final build.
Due to this as well implementing the art to the actual game took a hit, as many sprites where made for enemies, but never made it to the game as the combat system couldn't use multiple sprites until the end of production.
Here is some of the art created by Kornelija that didnt make it in due to the delays. unfortunatley there is more than this.
saving and loading work by creating two JSON files, one with temporary data of the play session, and another with the main save for the game, when playing the player will load and save on the temp one, but when the player reaches a checkpoint the temp save is pushed to the main save, this continued to change an be improved since the start of development as new things where implemented.
#region player menu system
// change layer of menu
public void PlayerMenuChangeLayer(int Layer)
{
playerMenuLayer = Layer;
PlayerMenuSetLayer(playerMenuLayer);
}
// open / change the menu layer
public void PlayerMenuOpen(InputAction.CallbackContext context)
{
if (context.performed)
{
if (playerMenuActive)
PlayerMenuChangeLayer(0);
else
PlayerMenuChangeLayer(1);
}
}
// set the layer
void PlayerMenuSetLayer(int layer)
{
switch (layer)
{
case 1:
playerMenuActive = true;
playerMenu.SetActive(true);
inventoryMenu.SetActive(true);
statusMenu.SetActive(false);
skillsMenu.SetActive(false);
Time.timeScale = 0;
break;
case 2:
inventoryMenu.SetActive(false);
statusMenu.SetActive(true);
skillsMenu.SetActive(false);
break;
case 3:
inventoryMenu.SetActive(false);
statusMenu.SetActive(false);
skillsMenu.SetActive(true);
break;
default:
playerMenuActive = false;
playerMenu.SetActive(false);
inventoryMenu.SetActive(false);
statusMenu.SetActive(false);
skillsMenu.SetActive(false);
Time.timeScale = 1;
break;
}
}
#endregion
Setting up the UI for the game took longer than expected, as you can see below with the menus here all menus can easily be used, they are run off a custom script that treats each menu like a layer, and pressing the buttons just changes the layer, this allows the player to use the escape key to go back without any heavy logic, adding extra menus is quick and only requires a few of lines of code.