How to Fix Save and Load Play Prefs - c#

I have a settings menu that has the option for a volume, viewdistance, and quality sliders. I want to save the value of those and use them when the player loads into the game. My issue is the quality level and view distance reset while the volume saves properly.
private void Start()
{
if(!PlayerPrefs.HasKey("musicVolume"))
{
PlayerPrefs.SetFloat("musicVolume",1);
Load();
}
else
{
Load();
}
if (!PlayerPrefs.HasKey("qualityLevel"))
{
PlayerPrefs.SetInt("qualityLevel",6);
Load();
}
else
{
Load();
}
if (!PlayerPrefs.HasKey("ViewDistance"))
{
PlayerPrefs.SetFloat("ViewDistance", 10);
Load();
Debug.Log("saved viewdistance" + qualityslider.value * 1000);
}
else
{
Load();
}
}
private void Save()
{
PlayerPrefs.SetFloat("musicVolume", volumeslider.value);
PlayerPrefs.SetInt("qualityLevel", QualitySettings.GetQualityLevel());
PlayerPrefs.SetFloat("ViewDistance", qualityslider.value);
}
private void Load()
{
volumeslider.value = PlayerPrefs.GetFloat("musicVolume");
qualityslider.value = PlayerPrefs.GetFloat("ViewDistance");
QualitySettings.SetQualityLevel(PlayerPrefs.GetInt("qualityLevel"));
}
public void ChangeVolume ()
{
Save();
AudioListener.volume = volumeslider.value;
}
public void ChangeViewDistance()
{
Save();
Camera.main.farClipPlane = viewDistanceSlider.value * 1000;
}
public void ChangeQuality(float level)
{
//level = qualityslider.value;
// QualitySettings.SetQualityLevel(level, false);
switch((float)level)
{
case 0:
QualitySettings.SetQualityLevel(0); // For lowest quality
break;
case 1:
QualitySettings.SetQualityLevel(1); // For lowest quality
break;
case 2:
QualitySettings.SetQualityLevel(2); // For lowest quality
break;
case 3:
QualitySettings.SetQualityLevel(3); // For lowest quality
break;
case 4:
QualitySettings.SetQualityLevel(4); // For lowest quality
break;
case 5:
QualitySettings.SetQualityLevel(5); // For lowest quality
break;
case 6:
QualitySettings.SetQualityLevel(6); // For lowest quality
break;
}
}
This is my first time trying save and load with player prefs in unity. I recieve no errors and can't find anything online. The volume saves but the view distance and quality do not.

The main mistake is
qualityslider.value = PlayerPrefs.GetFloat("ViewDistance");
That's the wrong slider! ;)
In general you Load the values into the sliders .. but you do not apply them to the actual systems!
Before coming to that some other comments.
In the ChangeQuality you are using a switch on a float value.
For a float it is mostly unlikely that they ever match an exact integer value! E.g. 0.5 + 0.5 might instead of the expected 1 rather be 1.0000001 or 0.99999999 in which case it won't enter the expected case.
You would probably rather do something like e.g.
var level = Mathf.Clamp(Mathf.RoundToInt(value), 0, 6);
QualitySettings.SetQualityLevel(level);
Besides that as mentioned before you are not calling Save within that method.
In general your entire Start method could be simplified a lot by simply providing fallback/default values like
private void Start ()
{
Load();
}
private void Load()
{
var volume = PlayerPrefs.GetFloat("musicVolume", 1);
var distance = PlayerPrefs.GetFloat("ViewDistance", 10);
var quality= PlayerPrefs.GetInt("qualityLevel", 6);
// ... See below
}
Then later on I would always only actually set the value that changed instead of everytime polling and saving them all.
Therefore you should use parameterized callbacks and do
public void ChangeVolume (float value)
{
AudioListener.volume = volumeslider.value;
PlayerPrefs.SetFloat("musicVolume", value);
}
public void ChangeViewDistance(float value)
{
Camera.main.farClipPlane = viewDistanceSlider.value * 1000;
PlayerPrefs.SetFloat("ViewDistance", value);
}
public void ChangeQuality(float value)
{
var level = Mathf.Clamp(Mathf.RoundToInt(value), 0, 7);
QualitySettings.SetQualityLevel(level);
PlayerPrefs.SetInt("qualityLevel", level));
}
And then finally coming back to the very first point:
Actually call all those once after loading!
private void Load()
{
var volume = PlayerPrefs.GetFloat("musicVolume", 1);
ChangeVolume(Volume);
var quality = PlayerPrefs.GetInt("qualityLevel", 6);
ChangeQuality(quality);
var distance = PlayerPrefs.GetFloat("ViewDistance", 10);
ChangeViewDistance(distance);
}

Your ChangeVolume() function calls Save() but your ChangeQuality() function does not.

Well, firstly,
You check if the key exists and if you don't set the value, you can just use PlayerPrefs.GetFloat("YourKey", DefaultValue) because when the player will need a different value they'll just change it in the settings and the new value will be read.
Secondly, you don't need to always update the preferences, only when they are changed, i. e. you change volume and save only volume, it's easier to write and read.
Thirdly, in ChangeQuality(float level) you can just do QualitySettings.SetQualityLevel(level) and use Mathf.Clamp(level, 0, 6) if you wanna restirct with the range from 0 to 6.
After doing that check if you call ChangeViewDistance() and ChangeQuality(), that's probably the whole cause of the problem.

To save the changed value of a slider to PlayerPrefs your public method must look like that
public void ChangeQuality(System.Single value) => PlayerPrefs.SetFloat("qualityLevel", Mathf.Clamp(value, 0, 6));
You need to use System.Single because it will help Unity automatically pass the new value, so you can save it.
Your method in the list must be under 'Dynamic float' label

Related

How can I change one material texture in an array?

How can I change one material in an array?
I've imported a Daz figure into unity, all of the materials are stored in an array for the figure.
I'm trying to change the texture of the Irises with the click of a button.
The code below does change the material but only on the first material in the array. I've attempted to alter the code to change the Irises, but I have been unable to.
{
//Component
private Renderer _rendereyes;
//Game Object
public GameObject eyes;
//etc
public Object[] texEyes;
public int texID;
// Start is called before the first frame update
void Start()
{
//Get Component
_rendereyes = eyes.GetComponent<Renderer>();
//Change eye tex
string texPath = "Textures";
texEyes = Resources.LoadAll(texPath, typeof(Texture2D));
}
public void SetEyeTexture()
{
if (texID < texEyes.Length - 1)
{
texID++;
}
else
{
texID = 0;
}
_rendereyes.material.SetTexture("_DiffuseMap",(Texture2D)texEyes[texID]);
}
Here is the 2nd iteration of the code which is my attempt to alter the Irises texture.
{
//Component
private Renderer[] _rendereyes;
//Game Object
public GameObject eyes;
//etc
public Object[] texEyes;
public int texID;
// Start is called before the first frame update
void Start()
{
//Get Component
_rendereyes = eyes.GetComponent<Renderer[]>();
//Change eye tex
string texPath = "Textures";
texEyes = Resources.LoadAll(texPath, typeof(Texture2D));
}
public void SetEyeTexture()
{
if (texID < texEyes.Length - 1)
{
texID++;
}
else
{
texID = 0;
}
_rendereyes[15].material.SetTexture("_DiffuseMap",(Texture2D)texEyes[texID]);
}
What is the best way to change the Irises texture?
First of all in general avoid using Resources at all. (See Best practices -> Resources)
Don't use it!
Then if you use it and in general I would be more specific and do
public Texture2D[] texEyes;
and then
texEyes = Resources.LoadAll<Texture2D>(texPath);
or rather simply drag and drop them into the slots via the Inspector
And finally
GetComponent<Renderer[]>
makes no sense. You always want to either get a single component or use GetComponents.
However, it sounds like you actually have only one single object with one single Renderer and want to stick to
_rendereyes = eyes.GetComponent<Renderer>();
or directly make it
[SerializeField] private Renderer _rendereyes;
and drag the object into the slot in the Inspector.
And then rather access the according index from Renderer.materials.
And then you have a little logical mistake in your SetEyeTexture method.
You do
if (texID < texEyes.Length - 1)
{
texID++;
}
which might result in texID = texEyes.Length which will be out o bounds since in c# all indices go from 0 to array.Length - 1.
Your code should rather look like
public void SetEyeTexture()
{
// Simple modulo trick to get a wrap around index
texID = (++texID) % texEyes.Length;
// Note that according to your screenshot the Material "Irises" is at index 14 not 15!
_rendereyes.materials[14].SetTexture("_DiffuseMap", texEyes[texID]);
}

Unity Question: How Can I Switch From One prefab To Another With GetAxis?

I want to bind the up and down arrow keys to cycle through different sprites upon being pressed. If one end is reached, it would loop back to the first sprite. I've tried using the following code:
public class PhaseChanger : MonoBehaviour
{
// saved for efficiency
[SerializeField]
public GameObject prefabMoon0;
[SerializeField]
public GameObject prefabMoon1;
[SerializeField]
public GameObject prefabMoon2;
[SerializeField]
public GameObject prefabMoon3;
[SerializeField]
public GameObject prefabMoon4;
// needed for new phase
GameObject currentPhase;
bool previousFramePhaseChangeInput = false;
/// <summary>
/// Start is called before the first frame update
/// </summary>
void Start()
{
currentPhase = Instantiate<GameObject>(prefabMoon0);
}
/// <summary>
/// Update is called once per frame
/// </summary>
void Update()
{
// change phase on up arrow or down arrow
if (Input.GetAxis("ChangePhase") > 0)
{
// only change phase on first input frame
if (!previousFramePhaseChangeInput)
{
previousFramePhaseChangeInput = true;
// Save current position and destroy current phase
Destroy(currentPhase);
// instantiate next phase
if (currentPhase = prefabMoon0)
{
currentPhase = Instantiate(prefabMoon1);
}
else if (currentPhase = prefabMoon1)
{
currentPhase = Instantiate(prefabMoon2);
}
else if (currentPhase = prefabMoon2)
{
currentPhase = Instantiate(prefabMoon3);
}
else if (currentPhase = prefabMoon3)
{
currentPhase = Instantiate(prefabMoon4);
else
{
// no phase change input
previousFramePhaseChangeInput = false;
}
}
}
}
When I attach the script to my main camera and run it, I'm able to make a single change with the up arrow, and then nothing else happens on subsequent presses.
I feel like I'm really close to making this work, but I also may being doing the whole thing inefficiently. Help would be much appreciated, thanks!
Also: I know I said sprites in my post and am sharing a script that calls on prefabs. I didn't know how to approach this using just the sprites without making a prefab for each. Is it possible to do this without separate prefabs for each sprite?
Problems
First of all you are using assignments
currentPhase = XY
where you should be using
currentPhase == XY
The reason why it still compiles is the implicit conversion operator for UnityEngine.Object -> bool. Basically your assigning equals writing
currentPhase = XY;
if(currentPhase)
It won't work like this either way because you are using Instantiate to create a new clone of a prefab which will of course have a different reference than the original prefab it was cloned from.
So even if your checks where looking like
if(currentPhase == XY)
they will ever be true.
Solution
Instead of checking for reference equality I would rather store all prefabs/instances in an array
public GameObject[] phases;
and then simply have an int index for this array so you can simply move to the next element from the array by increasing the index.
private int currentPhase;
And you can increase it and make it wrap around using e.g.
currentPhase = (currentPhase + 1) % phases.Length;
so it will always grow from 0 up to phases.Length - 1 and then start over from 0 again.
And then I don't know the exact requirements of your use case but I would suggest to rather not all the time use Instantiate and Destroy but rather have already all the objects as instances under your object and just (de)actívate them!
you could do this like e.g.
public GameObject[] phases;
private int currentPhase;
private void Awake ()
{
Init();
}
private void Update ()
{
if (Input.GetAxis("ChangePhase") > 0)
{
if (!previousFramePhaseChangeInput)
{
previousFramePhaseChangeInput = true;
NextPhase();
}
}
else
{
previousFramePhaseChangeInput = false;
}
}
// Disables all phases except the first one and sets the current index to 0
private void Init()
{
for(var i = 1; i < phases.Length; i++)
{
phases[i].SetActive(false);
}
phases[0].SetActive(true);
currentPhase = 0;
}
// Disables the current phase and enables the next one
// wraps around at the end of the array
public void NextPhase()
{
phases[currentPhase].SetActive(false);
// increase the counter and wrap around at the end of the array
currentPhase = (currentPhase + 1) % phases.Length;
phases[currentPhase].SetActive(true);
}
If you still want to Instantiate the objects because having them already in the scene is no option (for whatever reason) you could do it right before calling Init like e.g.
public GameObject[] phasePrefabs;
private GameObject[] phases;
private void Awake ()
{
var amount = phasePrefabs.Length;
phases = new GameObject [amount];
for(var i = 0; i < amount; i++)
{
phases[i] = Instantiate(phasePrefabs[i]);
}
Init();
}
Though as said I would prefer to already have them right away as this is way less error prone ;)

Monogame C# Timer (do something for 15 seconds every 3 seconds)

I am trying to create a timer, which, for example, every 3 seconds during eg 15 seconds will perform an action.
I tried to use gameTime.ElapsedGameTime.TotalSeconds and loop, but unfortunately it doesn't work.
I have an Attack () function that reduces player statistics when an enemy attacks it. I would like that in case of one particular enemy, this function for a specified period of time would subtract player's HP, eg for every 3 seconds. I guess it should be done in the Update function to access gameTime, unfortunately, I have no idea how to do it.
public override Stats Attack()
{
attack = true;
return new Stats(0, -stats.Damage, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
public override void Update(GameTime gameTime)
{
spriteDirection = Vector2.Zero; // reset input
Move(Direction); // gets the state of my keyborad
float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds; // make movement framerate independant
spriteDirection *= Speed; // add hero's speed to movement
position += (spriteDirection * deltaTime); // adding deltaTime to stabilize movement
totalPosition = new Vector2((int)((BottomBoundingBox.Center.X) / 32.0f), (int)((BottomBoundingBox.Center.Y) / 32.0f));
base.Update(gameTime);
}
I will make it simple, so you need to modify my code to achieve your desire result.
My best guess is that you want to have a special effect when your monsters hit your player.
First, you need to check if the monster actually hits the player (if collision is detected):
if (collision)//if it's true
{
// Apply your special effect if it is better than
// the one currently affecting the target :
if (player.PoisonModifier <= poisonModifier) {
player.PoisonModifier = poisonModifier;
player.ModifierDuration = modifierDuration;
}
//player.setColor(Color.Blue);//change color to blue
player.hitPoints -= Poision.Damage;//or enemy.PoisonDamage or whatever you define here
hit.Expire();//this can be for the arrow or bullet from your enemy or simply just a normal hit
}
In your Player class, you need:
public float ModifierDuration {
get {
return modifierDuration;
}
set {
modifierDuration = value;
modiferCurrentTime = 0;
}
}
Then in Update method of Player class:
// If the modifier has finished,
if (modiferCurrentTime > modifierDuration) {
// reset the modifier.
//stop losing HP code is here
modiferCurrentTime = 0;//set the time to zero
setColor(Color.White);//set back the color of your player
}
count += gameTime.ElapsedGameTime.TotalSeconds;//timer for actions every 3s
if (posionModifier != 0 && modiferCurrentTime <= modifierDuration) {
// Modify the hp of the enemy.
player.setHP(player.getCurrentHP() - posionDamage);
//Or change it to every 3s
//if (count > 3) {
// count = 0;
//DoSubtractHP(player);
//}
// Update the modifier timer.
modiferCurrentTime += (float) gameTime.ElapsedGameTime.TotalSeconds;
setColor(Color.Blue);//change the color to match the special effect
}
Hope this helps!
You need to store the start time, or the last time that the action was carried out. Then during each update compare the elapsed time to the stored time. If 3 seconds have passed then perform the action, store the current time and repeat the process.
I do not know monogame, but if I were doing this in one of my C# applications, I would use a timer, and pass in anything that the timer would need to modify.
There is good info here https://learn.microsoft.com/en-us/dotnet/api/system.timers.timer?view=netframework-4.8 and I stole a bit of code from here and modified it as an example to demonstrate my idea. I extended the System.Timer to allow it to run for a duration and stop itself. You can set the frequency and duration and forget about it. Assuming that you are able to update this information from a timer.
class Program
{
private static FixedDurationTimer aTimer;
static void Main(string[] args)
{
// Create a timer and set a two second interval.
aTimer = new FixedDurationTimer();
aTimer.Interval = 2000;
// Hook up the Elapsed event for the timer.
aTimer.Elapsed += OnTimedEvent;
// Start the timer
aTimer.StartWithDuration(TimeSpan.FromSeconds(15));
Console.WriteLine("Press the Enter key to exit the program at any time... ");
Console.ReadLine();
}
private static void OnTimedEvent(Object source, System.Timers.ElapsedEventArgs e)
{
FixedDurationTimer timer = source as FixedDurationTimer;
if (timer.Enabled)
{
Console.WriteLine("The Elapsed event was raised at {0}", e.SignalTime);
}
}
public class FixedDurationTimer : System.Timers.Timer
{
public TimeSpan Duration { get; set; }
private Stopwatch _stopwatch;
public void StartWithDuration(TimeSpan duration)
{
Duration = duration;
_stopwatch = new Stopwatch();
Start();
_stopwatch.Start();
}
public FixedDurationTimer()
{
Elapsed += StopWhenDurationIsReached;
}
private void StopWhenDurationIsReached(object sender, ElapsedEventArgs e)
{
if (_stopwatch != null && Duration != null)
{
if (_stopwatch.Elapsed > Duration)
{
Console.WriteLine("Duration has been met, stopping");
Stop();
}
}
}
}
}
You could see examples of how to pass objects into the timer here (#JaredPar's example) How do I pass an object into a timer event?
string theString = ...;
timer.Elapsed += (sender, e) => MyElapsedMethod(sender, e, theString);
static void MyElapsedMethod(object sender, ElapsedEventArgs e, string theString) {
...
}
One way to do this would be to use coroutines. MonoGame does not have built-in support for them like other game engines, but they are not too complicated to implement yourself. You need some knowledge of the yield keyword and enumerators to understand them, but once abstracted away they make your game code way easier to write and understand.
Here's an example of what your gameplay logic would look using a Coroutine system like the one described below:
public void Attack(Enemy enemyAttacking)
{
if (enemyAttacking.Type == "OneParticularEnemy")
{
StartCoroutine(RunDamageOverTimeAttack());
}
}
// This coroutine starts a second coroutine that applies damage over time, it
// then waits 15 seconds before terminating the second coroutine.
public IEnumerator RunDamageOverTimeAttack()
{
var cr = StartCoroutine(ApplyDamageOverTime());
yield return 15000; // in milleseconds (ms), i.e. 15000 ms is 15 seconds
cr.IsFinished = true;
}
// This coroutine applies the damage every 3 seconds until the coroutine is finished
public IEnumerator ApplyDamageOverTime()
{
while (true)
{
ApplyDamageToPlayer();
yield return 3000;
}
}
The code reads very close to the way you described the actual problem you're trying to solve. Now for the coroutine system...
The StartCouroutine method creates a Coroutine class instance and stores it. During the Update step of the game loop you iterate through the coroutines and update them, providing gameTime to calculate when the next step of the method should run. Each step executes the code in the routine until a yield is found OR until the method ends naturally. Once the coroutine is finished you clear them out. This logic looks something like this:
private List<Coroutine> coroutines = new List<Coroutine>();
public Coroutine StartCoroutine(IEnumerator routine)
{
var cr = new Coroutine(routine);
couroutines.Add(cr);
return cr;
}
public void UpdateCoroutines(GameTime gameTime)
{
// copied in case list is modified during coroutine updates
var coroutinesToUpdate = coroutines.ToArray();
foreach (coroutine in coroutinesToUpdate)
coroutine.Update(gameTime);
coroutines.RemoveAll(c => c.IsFinished);
}
public void Update(GameTime gameTime)
{
// normal update logic that would invoke Attack(), then...
UpdateCoroutines(gameTime);
}
A Coroutine class is responsible for tracking the time remaining between steps of the routine, and tracking when the routine is finished. It looks something like this:
public class Coroutine
{
private IEnumerator routine;
private double? wait;
public Coroutine(IEnumerator routine)
{
this.routine = routine;
}
public bool IsFinished { get; set; }
public void Update(GameTime gameTime)
{
if (IsFinished) return;
if (wait.HasValue)
{
var timeRemaining = wait.Value - gameTime.ElapsedGameTime.TotalMilliseconds;
wait = timeRemaining < 0 ? null : timeRemaining;
// If wait has a value we still have time to burn before the
// the next increment, so we return here.
if (wait.HasValue) return;
}
if (!routine.MoveNext())
{
IsFinished= true;
}
else
{
wait = routine.Current as double?;
}
}
}
This may seem considerably more complex than other solutions provided here, and it may be overkill, but Coroutines allow you to forgo tracking a bunch of state in tracking variables, making complex scenarios easier to follow and cleaner to read. For example, here's a arrow spawning strategy I used Coroutines for in Ludum Dare 37. It spawns 3 arrows 600 milleseconds apart with a 3 second wait between them: https://github.com/srakowski/LD37/blob/477cf515d599eba7c4b55c3f57952865d894f741/src/LD37/GameObjects/BurstArrowSpawnBehavior.cs
If you'd like more social proof of the value of Coroutines take a look at Unity. Unity is one of the more popular game engines, and it has Coroutine support. They describe a scenario where it is useful in their documentation: https://docs.unity3d.com/Manual/Coroutines.html.
I use this for my game :
Public Async Function DelayTask(Time As Double) As Threading.Tasks.Task
Await Threading.Tasks.Task.Delay(TimeSpan.FromSeconds(Time))
End Function
Converted to C# :
public async System.Threading.Tasks.Task DelayTask(double Time)
{
await System.Threading.Tasks.Task.Delay(TimeSpan.FromSeconds(Time));
}
You would use it like this in an Async Function :
Await DelayTask(1.5);
The number is in seconds, you can change this by changing the TimeSpan.whateverformat.
Considering that you'll have various things that affect your stats maybe you're better off at having an update subroutine in your Stats class that will check a list of effects that are scheduled to update after one point in time.
This would be better for performance than having each effect relying on its own thread.

Unity: flash at frequency with variable on/off ratio

I want to be able to flash stuff at a certain frequency. For an example, let's say 2Hz. I also want to be able to specify a ratio, where I can have the thing displayed for let's say 2/3 of the cycle and have it hidden for 1/3, so the ratio would be 2:1. It's a wild bunch of flashing, so I Need to stay flexible in the way I do it. There might be some flashing with a ratio of 3:5 and a frequency of 2Hz, and some other flashing at 4Hz with ratio 1:1, and so on.
Also, I need to be able to flash in sync. So if one object is flashing already and I start flashing another one, they need to be in sync (or rather their cycles need to be in sync, the flashing may vary as the ratio may be different). But if at the same frequency, they need to "turn on" at the same time, even if their ratios are different. Also, they all need to turn on at the same time the slowest turns on.
My current approach: I have a GameObject FlashCycle, that essentially in it's update method calculates a progress for the 3 frequency's I have (2Hz, 4Hz and 8Hz).
float time = Time.time;
twoHerzProgress = (time % twoHerzInSeconds) / twoHerzInSeconds;
fourHerzProgress = (time % fourHerzInSeconds) / fourHerzInSeconds;
eightHerzProgress = (time % eightHerzInSeconds) / eightHerzInSeconds;
I have tried different times, but that didn't really matter so let's just stick to that one if you don't think it's a bad idea!
Now, whenever I want to flash an object, in it's own Update() I do this:
switch (flashRate.herz)
{
case FlashRateInterval.twoHerz:
show = flashCycle.oneHerzProgress <= onTimePercentage;
case FlashRateInterval.fourHerz:
show =flashCycle.twoHerzProgress <= onTimePercentage;
case FlashRateInterval.eightHerz:
show =flashCycle.fourHerzProgress <= onTimePercentage;
default:
show =true;
}
and then just continue and have the object displayed if show == true.
Unfortunately this doesn't flash the objects at a nice smooth and regular interval. I measured the 2Hz interval and got differences in the ratio of up to 48ms, and though it seems like not much it really makes a difference on the screen.
So the question boils down to: How can I get quick, reqular flashes while maintaining the flexibility (ratio and frequency wise) and have a syncronized flash?
Thanks for your help!
You could use Coroutines and WaitForSeconds to achieve that
// onRatio and offRatio are "optional" parameters
// If not provided, they will simply have their default value 1
IEnumerator Flash(float frequency ,float onRatio = 1, float offRatio = 1)
{
float cycleDuration = 1.0f / frequency;
float onDuration = (onRatio/ (onRatio + offRatio)) * cycleDuration;
float offDuration = (offRatio/ (onRatio + offRatio)) * cycleDuration;
while(true)
{
show = true;
yield return new WatForSeconds(onDuration);
show = false;
yield return new WatForSeconds(offDuration);
}
}
so you can call it either with a frequency e.g. 8Hz
StartCoroutine(Flash(8.0f));
this is actually equal to any call where you set onRatio = offRatio e.g.
StartCoroutine(Flash(8.0f, onRatio = 1, offRatio = 1));
StartCoroutine(Flash(8.0f, onRatio = 2, offRatio = 2));
....
or with a frequency and ratios e.g. 1(on):2(off) with 8Hz
StartCoroutine(Flash(8.0f, onRatio = 1, offRatio = 2));
With this setup the Coroutine runs "forever" in the while(true)-loop. So, don't forget before you start a new Coroutine with different parameters to first stop all routines with
StopAllCoroutines();
Now if you want to change that dynamically in an Update method, you would have to add some controll flags and additional variables in roder to make sure a new Coroutine is only called when something changed:
FlashRateInterval currentInterval;
float currentOnRatio = -1;
float currentOffRatio = -1;
void Update()
{
// if nothing changed do nothing
if(flashRate.herz == currentInterval
//todo && Mathf.Approximately(<yourOnRatio>, currentOnRatio)
//todo && Mathf.Approximately(<yourOffRatio>, currentOffRatio)
) return;
StopAllCoroutines();
currentInterval = flashRate.herz;
//todo currentOnRatio = <yourOnRatio>;
//todo currentOffRatio = <yourOffRatio>;
switch (flashRate.herz)
{
case FlashRateInterval.twoHerz:
StartCoroutine(2.0f);
//todo StartCoroutine(2.0f, onRatio = <yournRatio>, offRatio = <yourOffRatio>);
case FlashRateInterval.fourHerz:
StartCoroutine(4.0f);
//todo StartCoroutine(4.0f, onRatio = <yournRatio>, offRatio = <yourOffRatio>);
case FlashRateInterval.eightHerz:
StartCoroutine(8.0f);
//todo StartCoroutine(8.0f, onRatio = <yournRatio>, offRatio = <yourOffRatio>);
default:
show =true;
}
}
Notes:
I dont know your FlashRateInterval but if you need to use it for some reason you could make it like
public enum FlashRateInterval
{
AllwaysOn,
twoHerz = 2,
fourHerz = 4,
eightHerz = 8
}
in order to directly use the correct values.
I would call a frequency variable flashRate.herz. You also wouldn't call a size value cube.meters. I'ld recommend to rename it to flashRate.frequency.
To archieve that syncing you would somehow need access to all Behaviours and compare their values (so I'ld say some static List<YourBehavior>) and than e.g. in the Coroutine wait until all bools are e.g. set to true before continuing with your own one. For that you would need an additional bool since it is possible that show is true permanently on one component.
public bool isBlinking;
IEnumerator Flash(float frequency ,float onRatio = 1, float offRatio = 1)
{
//todo: You'll have to set this false when not blinking -> in Update
isBlinking = true;
float cycleDuration = 1.0f / frequency;
float onDuration = (onRatio/ (onRatio + offRatio)) * cycleDuration;
float offDuration = (offRatio/ (onRatio + offRatio)) * cycleDuration;
// SYNC AT START
show = false;
// wait until all show get false
foreach(var component in FindObjectsOfType<YOUR_COMPONENT>())
{
// skip checking this component
if(component == this) continue;
// if the component is not running a coroutine skip
if(!component.isBlinking) continue;
// Now wait until show gets false
while(component.show)
{
// WaitUntilEndOfFrame makes it possible
// for us to check the value again already before
// the next frame
yield return new WaitForEndOfFrame;
}
}
// => this line is reached when all show are false
// Now lets just do the same but this time wating for true
// wait until all show get false
foreach(var component in FindObjectsOfType<YOUR_COMPONENT>())
{
// skip checking this component
if(component == this) continue;
// if the component is not running a coroutine skip
if(!component.isBlinking) continue;
// Now wait until show gets false
while(!component.show)
{
// WaitUntilEndOfFrame makes it possible
// for us to check the value again already before
// the next frame
yield return new WaitForEndOfFrame;
}
}
// this line is reached when all show are getting true again => begin of loop
while(true)
{
.........
Instead of using FindObjectsOfType<YOUR_COMPONENT>() which is kind of slow you could also do something like
public static List<YOUR_COMPONENT> Components = new List<YOUR_COMPONENT>();
private void Awake()
{
if(!Components.Contains(this)){
Components.Add(this);
}
}
so you also get currently disabled components and objects
You got some diferences because you are doing everything in an Update() cycle with <= condition. On slower/faster machines you will have more/less differences because the frame's duration will never be equal to your frequency.
Try doing everything in a Corotine: unity coroutine docs
//bad code below but i think its more understandable like this
IEnumerator Flash()
{
while(true)
{
BlinkOn();
Sync();//sync here another cicle if you want to sync when on starts
yield return new WaitForSeconds(yourDuration);// yourDuration*multiplier/something+0.5f....ecc
BlinkOff()
Sync();//sync here another cicle if you want to sync when of starts
yield return new WaitForSeconds(yourDuration);
}
}

Unity C# Saved Game via Binary Formatting not retaining Data

Update: This issue may be caused by binaryformatter's issues with editing data in existing fields. A comment that I can no longer find described alternate methods which I am currently implementing. Will update after these methods are attempted if it is a solution to the problem.
I should start by saying I am a student so please go easy on me, I got reported when I first started coming here and hope to keep learning. I an new to unity, my C# is decent, but full of gaps since my schooling was rather terrible. I have watched hundreds of hours of unity tutorials and am studying the new concepts I learn for 4 hours every night after I get out of work, so if you see something just let me know.
This is actually a problem I have had for a while, but thought I fixed months ago. I was attempting to save games for the first time, and read into binary formatting and such to save. I had problems getting it up, but I I managed to get it to save and pull properly from a file. I verify that the data going into the file is correct, and the data coming out is correct, and even made the data private with a control function so nothing will access and change it without jumping through my debug. And yet after I leave the scope where I define the data it changes, without anything accessing my update function.
To break it down I have a class called PlayerType that stores all player information including my scenemanager, and it serializes this and saves to a file as a list. I create a for loop using the current length of the loaded list using an instance of the saveload class (this is what holds the list of save games and the access to the file) and it loops through instantiating my buttons in the order. Slot 1 will click to save game 1 and so to speak. The issue I am having is clicking slot 1 clicks slot 16, so does slot 2. In face, it seems practically random which buttons go to which slot. I should say here I am not sure whether it is actually going to the wrong slot, or simply renaming the player names wrong, but either way it does not appear this should be the case.
Here is my load function
public void Load() //Loads file from .gd file after checking if exists, then deserializes it back into a list
{
accessDataPath(false);
Debug.Log("Size of Save File" + listSize);
for (int i = 0; i < listSize; i++)
{
Debug.Log("First Loop " + SavedGames[i].returnName());
}
foreach (PlayerType player in GameManager.instance.saveStorage.returnList())
{
Debug.Log("Second Loop" + player.returnName());
}
}
and here is my accessDataPath function
public void accessDataPath(bool isSave)
{
//This stucture checks if file exists or not, then checks if saving or loading for 4 outcomes
if (isSave && SavedGames == null)//if saving and save file to update has nothing in it
{
Debug.Log("Error attempted to save null list in SaveLoad.accessDataPath!");
}
if (File.Exists(Application.persistentDataPath + "/SavedGames.gd"))//if the file exists
{
if (isSave)//if you are saving
{
BinaryFormatter bf = new BinaryFormatter();
FileStream file = File.Open(Application.persistentDataPath + "/SavedGames.gd", FileMode.Open);
bf.Serialize(file, SavedGames);
file.Close();
}
else //if you are not saving
{
BinaryFormatter bf = new BinaryFormatter();
FileStream file = File.Open(Application.persistentDataPath + "/SavedGames.gd", FileMode.Open);
SavedGames = (List<PlayerType>)bf.Deserialize(file);
file.Close();
listSize = SavedGames.Count;
}
}
else//if the file does not exist
{
if (isSave)//if you are saving
{
BinaryFormatter bf = new BinaryFormatter();
FileStream file = File.Create(Application.persistentDataPath + "/SavedGames.gd");
bf.Serialize(file, SavedGames);
file.Close();
}
else//if you are not saving
Debug.Log("Error Loading File. Does not Exist!");//Display Later that file does not exist to user
}
updateNames();
}//End accessDataPath
here is the function that updates the names
public void updateNames()
{
for (int i = 0; i < listSize; i++)
{
SavedGames[i].updateName("Player " + i);
Debug.Log("Bump " + i);
}
}
As you can see I verify if it exists, then check if I am saving or loading. The save function calls with a true and SavedGames is the list that is pulled from the file. Now After I run the name change I can check to see if it worked, and it does. Running the check here they all come back as the proper name, however by the time it gets back to my load function and runs my first loop they are wrong, it never leaves this section of code but as soon as it exits the scope they change to be almost random.
Now I had this problem for a while, but thought it had to do with my file maybe having corrupt data, so I deleted the save game and created new ones to test. The numbers changed, but were still not right. This tells me that somehow the file is effecting the names even though I rename them almost immediately after they come out. I am not used to loading or saving to files so I am not sure where to start here.
I will post more of my code below in case some of it stands out as being blatantly wrong, I also appreciate advice on structure as I took college classes on game programming but they were terrible and filled with gaps even if you don't spot the answer to my problem.
First I have my menu system setup to be toggled on or off, called by onclick events.
public class SaveMenu : MonoBehaviour {
public GameObject menu;
bool isActive;
bool isSave;
PopulateSave playerSaves;
public static SaveMenu instance;
void Awake()
{
isSave = false;
instance = this;
menu = GameObject.Find ("SaveList");
menu.SetActive (false);
isActive = false;
playerSaves = menu.GetComponentInChildren<PopulateSave> ();
}
public bool getActive()
{
return isActive;
}
public void toggleMenu(bool saveLoad)
{
if (isActive)
{
menu.SetActive (false);
isActive = false;
} else
{
menu.SetActive (true);
isActive = true;
}
bool isSave = saveLoad;
menu.transform.Find("Scroll View").transform.Find("Viewport").transform.Find("Content").transform.Find("NewSave").gameObject.SetActive(isSave);
Debug.Log(isSave);
}
public void updateSave()
{
playerSaves.startList();//Seems redundant but is used to make this access public with limited use
}
public bool getSave()
{
return isSave;
}
}
I call populatesaves as a child after everything is created because I had issues with it not existing when I tried to bring it in via the inspector. PopulateSaves is where most of my functional code is.
public class PopulateSave : MonoBehaviour{
public Button NewSave;
public Button OldSaves; // This is our prefab object that will be exposed in the inspector
void Awake()
{
GameManager.instance.saveStorage.Load();
Populate();
}
void Update()
{
}
void Populate()
{
Button newObj = Instantiate(NewSave, transform); // Create GameObject instance
newObj.name = "NewSave";
startList();
}
public void startList()
{
clearList();
for (int i = 1; i < GameManager.instance.saveStorage.returnCount() + 1; i++)
{
createButton(i);
}
Debug.Log("Done creating");
}
public void updateList(PlayerType newSave)
{
Button newObj;
newObj = (Button)Instantiate(OldSaves, transform);
//GameManager.instance.saveStorage.Save(i);
}
public void clearList()
{
GameObject[] gameObjects;
gameObjects = GameObject.FindGameObjectsWithTag("SaveSlot");
for (var i = 0; i < gameObjects.Length; i++)
Destroy(gameObjects[i]);
Debug.Log("Done Destroying");
}
public void ButtonClicked(int slot)
{
if (SaveMenu.instance.getSave())
{
Debug.Log("Save");
GameManager.instance.saveStorage.Save(slot);
}
else
{
Debug.Log("load");
GameManager.instance.currentPlayer.newPlayer(slot);
}
}
public void createButton(int i)
{
// Create new instances of our prefab until we've created as many as is in list
Button newObj = (Button)Instantiate(OldSaves, transform);
//increment slot names
newObj.name = "Slot " + i;
newObj.GetComponentInChildren<Text>().text = "Slot " + i;
newObj.onClick.AddListener(() => ButtonClicked(i));
}
}
Now I also have never written a listener with a script like I have here, could that section be assigning the wrong number to them? I checked to make sure all my indexes had the right numbers, if not being 1 off. One of my loops might use a setup that is 1 off but I am more worried about getting this off the ground and fixing the specifics at this point.
Here is my player storage
[System.Serializable]
public class PlayerType {
string playerName;
SceneManagement currentScene;
public PlayerType()
{
playerName = "Starting Name";
}
public void updateName(string name)
{
//Debug.Log("New Name: " + name);
playerName = name;
}
public void updateScene(SceneManagement newScene)
{
currentScene = newScene;
}
public string returnName()
{
return playerName;
}
public SceneManagement returnScene()
{
return currentScene;
}
public void newPlayer(int slotSave)
{
//Tmp player has wrong name from this point, initiated wrong?
//Debug.Log("New Name to update" + tmpPlayer.returnName());
this.updateName(GameManager.instance.saveStorage.returnSaves(slotSave).returnName());
this.updateScene(GameManager.instance.saveStorage.returnSaves(slotSave).returnScene());
}
}
Update:
Bump correctly goes through displaying 0-16
then Size of Save File displays 17 total saves
First loop then starts by outputting 'First Loop Player 15' a total of 16 times
Then it displays 16 so the last one is correct, though one off I guess.
Second loop does the same as first, unsurprisingly.
I left the call to updateNames in but commented out the lines and ran it including taking out bump.
It starts with 17 saves again and the 16 time iteration of player 15, however this time around the last one displays 'Temp Name' which I only define once at the beginning of my sceneManagement script for the current player, it should have never been saved, and even if it had been should have been overwritten by my name loop, at least that was my intent.
SceneManagement
[System.Serializable]
public class SceneManagement : MonoBehaviour {
DialogueManager dialogueManager;
PlayerType currentPlayer;
bool isSave;
bool isActive;
string sceneName;
int lineCount;
void Start()
{
isSave = false;
//loads player object for the current save
currentPlayer = new PlayerType();
currentPlayer.updateName ("Temp Name");
//This loads the prologue from the DB and sets the dialoguemanager up, defaults to prologue for now but can be updated to another scene later
isActive = true;
sceneName = "Prologue";
string conn = "URI=file:" + Application.dataPath + "/Text/Game.sqlite3";
List<string> sceneLines = new List<string>();
List<string> sceneCharacters = new List<string>();
int tmpInt;
string tmpString = "NOTHING";
int count = 0;
lineCount = 1;
IDbConnection dbConn;
dbConn = (IDbConnection)new SqliteConnection (conn);
dbConn.Open (); //Open database connection
IDbCommand dbCmd = dbConn.CreateCommand();
string sqlQuery = "SELECT Line, Flags, Character, Image, Text, Color FROM Prologue";
dbCmd.CommandText = sqlQuery;
IDataReader reader = dbCmd.ExecuteReader ();
while (reader.Read ()) {
tmpInt = reader.GetInt32 (0);
tmpString = reader.GetString (1);
sceneCharacters.Add(reader.GetString (2));
tmpInt = reader.GetInt32 (3);
sceneLines.Add(reader.GetString (4));
tmpString = reader.GetString (5);
//Debug.Log (tmpString);
//Debug.Log (count);
count++;
}
reader.Close ();
reader = null;
dbCmd.Dispose ();
dbCmd = null;
dbConn.Close ();
dbConn = null;
dialogueManager = new DialogueManager(sceneCharacters, sceneLines, lineCount);
}
//These are the returnvalues that might be used, may or may not be kept depending on future use.
public string getSceneName()
{
return sceneName;
}
public int getLineCount()
{
return lineCount;
}
public bool getSave()
{
return isSave;
}
//These return the lines for displaymanager, preventing it from directly interacting with dialoguemanager
public string getNextLine()
{
return dialogueManager.getNextLine();
}
public string getNextCharacter()
{
return dialogueManager.getNextCharacter();
}
//This function sets up visibility and is only accessed to properly display the screen after the scene has been started.
public void startScene ()
{
GameManager.instance.screenDisplay.changeVisible(true);
}
void Update()
{
if (Input.GetKeyDown ("space") & isActive) {
dialogueManager.incrementCurrentLine ();
GameManager.instance.screenDisplay.changeText(dialogueManager.getNextLine ());
GameManager.instance.screenDisplay.changeCharacter(dialogueManager.getNextCharacter ());
}
if (Input.GetKeyDown ("escape")) {
if (!PauseMenu.instance.getActive () && !SaveMenu.instance.getActive ())
{
PauseMenu.instance.toggleMenu ();
}
else if (SaveMenu.instance.getActive())
{
SaveMenu.instance.toggleMenu (true);
PauseMenu.instance.toggleMenu ();
}
else if (PauseMenu.instance.getActive())
{
PauseMenu.instance.toggleMenu ();
}
}
}
public void initialScreen()
{
SceneManager.sceneLoaded += OnLevelFinishedLoading;
}
void OnDisable()
{
SceneManager.sceneLoaded -= OnLevelFinishedLoading;
}
void OnLevelFinishedLoading (Scene scene, LoadSceneMode mode)
{
GameManager.instance.screenDisplay.initialScreen(dialogueManager.getNextLine(), dialogueManager.getNextCharacter(), null, null,
null, null, null, null);
}
public PlayerType returnPlayer()
{
return currentPlayer;
}
}
while im at it here is my GameManager too, though I havent messed with it a ton. Mostly using it as a DontDestroyOnLoad thing to hold everything else at this point.
public class GameManager : MonoBehaviour{
public static GameManager instance;
public DisplayScreen screenDisplay;
public SceneManagement managerScene;
public SaveLoad saveStorage;
public PlayerType currentPlayer;
//leave out until load data is setup
//PlayerType currentPlayer;
void Awake()
{
instance = this;
DontDestroyOnLoad (transform.gameObject);
managerScene = gameObject.AddComponent(typeof(SceneManagement)) as SceneManagement;
screenDisplay = gameObject.AddComponent (typeof(DisplayScreen)) as DisplayScreen;
saveStorage = gameObject.AddComponent (typeof(SaveLoad)) as SaveLoad;
//initialize player and sceneManager here, but only load through main menu options
//of new game or load game
}
void update()
{
}
}
I have continued troubleshooting and brought it down to a particularly confusing part for me. I took out all debugging logs and added 3 loops into my load function, they are these.
Debug.Log("LOAD CALLED");
accessDataPath(false);
for (int i = 0; i < listSize; i++)
{
Debug.Log("First Loop " + SavedGames[i].returnName());
SavedGames[i].updateName("Updated Player " + (i + 1));
}
for (int i = 0; i < listSize; i++)
{
Debug.Log("Second Loop " + SavedGames[i].returnName());
}
foreach (PlayerType player in GameManager.instance.saveStorage.returnList())
{
Debug.Log("Third Loop " + player.returnName());
}
So the first one displays player 15, then correctly displays the first loops 1-15 then temp name again, still havent 'figured out why that is popping up but I think it is related. Then the second loop iterates all wrong. Literally changed and back to back the loops are wrong, the only difference being it left the scope of the for loop. I ran a third loop using foreach to see if the type of call made a difference and it does not.
Even changing the name, and then immediately calling a loop to check shows that the values are changing. I think it might have something to do with how I am storing the objects, but I am not sure how the problem could be arising. Its not going null, and the names are changing to be the same every time so it isn't completely random. I am posting this here just after I found this hoping that soon I will solve it, but am also hoping if I do not someone else might spot something. I have the next 3 hours to work on it so I will be trying this entire time checking back every now and then. Thanks in advance for anyone that might glance at it for me.
Ok so I finally got the implementation done. I had to swap around a ton of my code, and ended up doing a wrapper object/list combination using JSON. Essentially the problem is that binary formatter messes up objects after you de-serialize them. Every time I updated my objects values they would randomly change on me for no reason without being accessed.
This was surprising as about half the posts I read on saving say its good, others like This one say that it is bad practice. I had done some research initially but had not come across the negative aspects of using it. I am still having problems, but Json is successfully retaining data and my objects are not messing up. I am pretty sure this was the problem as the only sections that I changed were my objects value structure to public for the serializing, and implemented the json structure into my SaveLoad script. This video was very helpful for the overall structure and getting started, and This thread helped me with troubleshooting when I ran into several problems.
I should also note that one thing I did not catch for a while. While Json can load lists, the initial object to be loaded must not be a list. I was attempting to save a list of my PlayerType directly into a folder, which it will not do. I ended up creating a quick object that contained my list and then saving the object. Since everywhere I read said that lists were fine it took a while to discover that this was causing part of my problem. It was not giving me any errors, just returning a blank string which most threads said was because it was not public or serializable.
Anyway here is to hoping my struggles and searches for answers might help as the things I found were quite scattered and hard to come across.

Categories