Charactersheet.
@ThePersister In my game i have a menu where you can create a party. I have an array of createcharacter buttons that bring you to a character creation screen where you can distribute attribute points etc. these points right now are not saved anywhere and are just displayed in text elements. when the player is done distributing points he can click on the next button and equip his character with a weapon and armor. this is where i am stuck i want that after the player is done the character can be created by clicking the create character button. the data is then saved to somekind of charactersheet and the player is brought back to the first menu. where the first button he clicked is now occupied bij the name of the character he just created. and he can create other character that can be added to the party. I have watched alot of tutorials now and i am sort of confused what i have to create. do i need a database for every character or one database for all characters. i figured i could use a get string to get the values of the attributes and add them to the charactersheet. what is the technique called to create said database so i can lookup some tutorials on how to create one. this is the final hurdle i have to take before i can go make some actual gameplay elements :D I am sorry for the huge wall of text! All help is appreciated!
Answer by ThePersister · Nov 18, 2016 at 10:55 PM
Hey @Furcifer-bellator !!
Thank you for the direct question, I'm flattered :)
I created a Save System Example for you based on this other post: http://answers.unity3d.com/questions/610893/how-do-i-save-a-custom-class-of-variables-to-playe.html
@iwaldrop showed a beautiful example for a Serializer that can be used to save data. I created an example that applies this Serializer with some slight adjustments.
Download the package here! https://www.dropbox.com/s/tmzf6vtc73wloom/SaveLoadExample.unitypackage?dl=0
Here's the visual preview:
Here's the Code (Explained at bottom of the post):
Serializer:
using System;
using System.IO;
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
public class Serializer
{
private const string m_fileExtension = ".dat";
private const string m_prefix = "SaveSystemExample_";
private static string _getFullStorePath(string filename)
{
return Application.persistentDataPath + "/" + m_prefix + filename + m_fileExtension;
}
public static T Load<T>(string filename) where T : class
{
if (File.Exists(_getFullStorePath(filename)))
{
try
{
using (Stream stream = File.OpenRead(_getFullStorePath(filename)))
{
BinaryFormatter formatter = new BinaryFormatter();
return formatter.Deserialize(stream) as T;
}
}
catch (Exception e)
{
Debug.Log(e.Message);
}
}
return default(T);
}
public static void Save<T>(string filename, T data) where T : class
{
using (Stream stream = File.OpenWrite(_getFullStorePath(filename)))
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, data);
}
}
}
SaveSlot:
using UnityEngine;
using System.Collections;
public class SaveSlot : MonoBehaviour
{
public string m_boundFileName = "SaveSlotX";
}
Character (Example class to save, that fits your context):
using UnityEngine;
using System;
[Serializable]
public class Character {
[Header("Meta")]
public string m_name = "Lex";
[Header("Attributes")]
[Range(0f, 10f)]
public int m_strength = 7;
[Range(0f, 10f)]
public int m_intelligence = 9,
m_agility = 8,
m_endurance = 7,
m_luck = 10;
[Header("Weapon")]
public Weapon m_currentWeapon = new Weapon("Dragonslayer Blade", 9001f);
[Header("Armor")]
public Armor m_currentArmor = new Armor("Dragonscale Armor", 0.4f);
}
[Serializable]
public class Weapon
{
public string m_name;
public float m_dps; // Damage per second.
public Weapon(string name, float dps)
{
m_name = name;
m_dps = dps;
}
}
[Serializable]
public class Armor
{
public string m_name;
[Range(0f,1f)]
public float m_durability01,
m_damageReduction01;
public Armor(string name, float damageReduction01)
{
m_name = name;
m_damageReduction01 = Mathf.Clamp01(damageReduction01);
m_durability01 = 1.0f; // Starts at full durability.
}
}
SaveSystem (Saving, Loading and a bit of UI, cut away what you don't need):
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using System.Linq;
public class SaveSystem : MonoBehaviour {
public ToggleGroup m_saveSlotsGroup;
public Character m_currentCharacter;
void Start()
{
// Load existing saveSlots' data.
SaveSlot[] saveSlots = m_saveSlotsGroup.GetComponentsInChildren<SaveSlot>();
foreach (SaveSlot saveSlot in saveSlots)
{
LoadSaveSlot(saveSlot);
}
}
public void Save()
{
SaveSlot selectedSaveSlot = _getSelectedSaveSlot();
Serializer.Save<Character>(selectedSaveSlot.m_boundFileName, m_currentCharacter);
_updateCharacterText(selectedSaveSlot);
}
public void Load()
{
SaveSlot selectedSaveSlot = _getSelectedSaveSlot();
LoadSaveSlot(selectedSaveSlot);
}
private void LoadSaveSlot(SaveSlot saveSlot)
{
m_currentCharacter = Serializer.Load<Character>(saveSlot.m_boundFileName);
if (m_currentCharacter != null)
{
// Succesfully loaded character:
_updateCharacterText(saveSlot);
}
}
private void _updateCharacterText( SaveSlot saveSlot )
{
// Visual update for the button Label.
saveSlot.GetComponentInChildren<Text>().text = m_currentCharacter.m_name;
}
private SaveSlot _getSelectedSaveSlot()
{
if (m_saveSlotsGroup.AnyTogglesOn())
{
Toggle activeToggle = m_saveSlotsGroup.ActiveToggles().First();
SaveSlot selectedSaveSlot = activeToggle.GetComponent<SaveSlot>();
if (selectedSaveSlot == null)
{
throw new System.NullReferenceException("SaveSlot was null, check whether the active toggle had a SaveSlot script on it");
}
else
{
return selectedSaveSlot;
}
}
else
{
throw new System.NullReferenceException("To do anything with SaveSlots, you'll have to select one first.");
}
}
}
Basically:
The Serializer class is the main code you need for loading and saving, everything else is just data / filename binding.
The Character class is an example of a class you could save.
The SaveSlot class just hold a filename string that's used to save and load at a specific location.
The SaveSystem has a ToggleGroup reference and gets the selected SaveSlot from it (the Toggles each have a SaveSlot component), then it gets the filename defined in the SaveSlot and uses that to Load and/or Save the current Character data, lastly updates the text of the toggles for better visual feedback!
The two buttons shown in the preview call the Load and Save methods in the SaveSystem to trigger a Save or Load using the selected SaveSlot.
I hope all of that made sense!
Thank you for another cool question, I enjoyed making this one and I might need something similar myself in the future, so thank you! ;)
If this helped, please accept the answer, it'd be much appreciated!
Best of luck!
Cheers,
ThePersister
Thankyou! this is almost exactly what i was looking for! i only have one little problem. i have the setup working in my own game like it does in your example. it works really great! except i have one problem in my game when you click the create character button it brings the player to another screen to spend attribute points. i hooked this up in a way that when you click the button it sets the activity of the current screen to null and activates the next screen. i guess you can't save in a slot on an inactive panel and i have to hook it up diffrently. do you have any suggestion on how i should hook it up? i am thinking i may have to have all screens active at all times and have them layed out next to eachother and just have the screen move between them when you click the button.
That's luckily easily fixed!
You just have to store a string with the file name to save to before loading the character creation screen.
Then upon completion, use that saved string to save the created character.
You would add the following code for that:
private string m_filenameToSaveTo; // Fill upon starting create character.
public void CreateCharacter()
{
m_filenameToSaveTo = _getSelectedSaveSlot().m_boundFileName;
// Disable SaveSlot selection UI.
// Load Character create screen.
}
public void SaveCharacter()
{
// Upon being done with Character Creation. (Asu$$anonymous$$g you've been editing m_currentCharacter)
// (You can also create a new Character here and fill it with the data from the UI).
Serializer.Save<Character>(m_filenameToSaveTo, m_currentCharacter);
// Saved Character, now start the game.
}
If you do it that way, you should be able to keep everything intact!
Cheers,
ThePersister
Answer by Furcifer-bellator · Nov 21, 2016 at 04:23 PM
Hey @ThePersister!
I added the new scriptlines to the saveSystem script. i haven't yet got it to work. i don't understand the comment in the script: //Fill upon starting create character I have set it up like this: 2 saveslot buttons: one character creation button that has the character creation script event added and disables the current screen and opens the charactercreator screen.
at this point i'm just testing it with the character creator that you created.
but when i change the name of the character and go back to the previous screen the buttons are not updated. i'm not sure if the savecharacter function is supposed to have an update text function in it.
i know i am way over my head in this one but you've helped me alot allready and i can almost start creating gameplay elements! btw sorry to keep bothering you. (script for refference below)
void Start()
{
// Load existing saveSlots' data.
SaveSlot[] saveSlots = m_saveSlotsGroup.GetComponentsInChildren<SaveSlot>();
foreach (SaveSlot saveSlot in saveSlots)
{
LoadSaveSlot(saveSlot);
}
}
public void Save()
{
SaveSlot selectedSaveSlot = _getSelectedSaveSlot();
Serializer.Save<Character>(selectedSaveSlot.m_boundFileName, m_currentCharacter);
_updateCharacterText(selectedSaveSlot);
}
public void Load()
{
SaveSlot selectedSaveSlot = _getSelectedSaveSlot();
LoadSaveSlot(selectedSaveSlot);
}
private void LoadSaveSlot(SaveSlot saveSlot)
{
m_currentCharacter = Serializer.Load<Character>(saveSlot.m_boundFileName);
if (m_currentCharacter != null)
{
// Succesfully loaded character:
_updateCharacterText(saveSlot);
}
}
private void _updateCharacterText( SaveSlot saveSlot )
{
// Visual update for the button Label.
saveSlot.GetComponentInChildren<Text>().text = m_currentCharacter.m_name;
}
private SaveSlot _getSelectedSaveSlot()
{
if (m_saveSlotsGroup.AnyTogglesOn())
{
Toggle activeToggle = m_saveSlotsGroup.ActiveToggles().First();
SaveSlot selectedSaveSlot = activeToggle.GetComponent<SaveSlot>();
if (selectedSaveSlot == null)
{
throw new System.NullReferenceException("SaveSlot was null, check whether the active toggle had a SaveSlot script on it");
}
else
{
return selectedSaveSlot;
}
}
else
{
throw new System.NullReferenceException("To do anything with SaveSlots, you'll have to select one first.");
}
}
// new addition:
private string m_filenameToSaveTo;
// Fill upon starting create character.
public void CreateCharacter()
{
m_filenameToSaveTo = _getSelectedSaveSlot().m_boundFileName;
// Disable SaveSlot selection UI.
// Load Character create screen.
}
public void SaveCharacter()
{
// Upon being done with Character Creation. (Asuming you've been editing m_currentCharacter)
// (You can also create a new Character here and fill it with the data from the UI).
Serializer.Save<Character>(m_filenameToSaveTo, m_currentCharacter);
Debug.Log ("you saved a character");
// Saved Character, now start the game.
}
}