Skip to content

Instantly share code, notes, and snippets.

@RiskyWilhelm
Created November 29, 2024 13:50
Show Gist options
  • Save RiskyWilhelm/1ad143dc07764a01b3d2ae6e868c5485 to your computer and use it in GitHub Desktop.
Save RiskyWilhelm/1ad143dc07764a01b3d2ae6e868c5485 to your computer and use it in GitHub Desktop.
Unity Utils (C# >= 9)
using System;
public static class DateTimeUtils
{
/// <summary> By default, <see cref="DateTimeExtensions"/> and <see cref="DateTimeUtils"/> will offset the hour by this value to match Cardinal Direction. Means, degree of zero will equal to 18:00 in a circle </summary>
public const float DefaultOffsetHour = 18f;
public const float ExactHourDegree = (360f / 24f); // 1 hour = 15 degree
public const float ExactMinutePoint = (ExactHourDegree / 60f); // 1 min = 0,25 degree
public const float ExactSecondPoint = (ExactMinutePoint / 60f); // 1 second = 0,004166 degree
// TODO: Find Year Month and Day from degree
/// <remarks> Assumes the time 12:00 equals North(Y+ when facing to Z+) in cardinal direction. Means, degree of zero will equal to 18:00 in a circle </remarks>
public static DateTime AngleDegreeToDateTime(float timeDegree, sbyte offsetHour = 0)
{
float offsetHourDegree = (offsetHour + DefaultOffsetHour) * ExactHourDegree;
float verticallyMirroredDegree = (360f - timeDegree); // Angle is not inverted. Mirrored the angle in a circle vertically. That way, time will grow if rotation getting negative
float calculatedDegree = (verticallyMirroredDegree + offsetHourDegree) % 360f;
int hour = (int)(calculatedDegree / ExactHourDegree);
calculatedDegree -= (hour * ExactHourDegree);
int minute = (int)(calculatedDegree / ExactMinutePoint);
calculatedDegree -= (minute * ExactMinutePoint);
int second = (int)(calculatedDegree / ExactSecondPoint);
return new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, hour, minute, second);
}
}
using System;
public static class EnumUtils
{
/// <summary> Throws exception on mismatch </summary>
public static void ValidateUnderlyingType<EnumType, EnumUnderlyingType>()
where EnumType : Enum
where EnumUnderlyingType : struct
{
if (Enum.GetUnderlyingType(typeof(EnumType)) != typeof(EnumUnderlyingType))
throw new NotSupportedException("Underlying value Type of EnumType and EnumUnderlyingType must be same and enums only support; byte, sbyte, short, ushort, int, uint, long, or ulong");
}
public static void ValidateFlagEnum<T>()
where T : Enum
{
if (!typeof(T).IsDefined(typeof(FlagsAttribute), inherit: false))
throw new NotSupportedException("Only flag enums with Flags attribute supported");
}
}
using System;
using UnityEngine;
public static class EventReflectorUtils
{
/// <returns> If found, returns the reflected GameObject, else self </returns>
public static bool TryGetReflectedGameObject(GameObject gameObject, out GameObject reflectedTo)
{
if (gameObject.TryGetComponent<EventReflector>(out EventReflector foundEventReflector))
{
reflectedTo = foundEventReflector.reflected;
return true;
}
reflectedTo = gameObject;
return false;
}
/// <summary> Same with <see cref="GameObject.TryGetComponent{T}(out T)"/> except it reflects the method to desired <see cref="GameObject"/> via <see cref="EventReflector"/> if there is any </summary>
public static bool TryGetComponentByEventReflector<TargetType>(GameObject searchGameObject, out TargetType foundTarget)
{
// Check if event wants to reflect the collision. If there is no EventReflector, it is the main object that wants the event
TryGetReflectedGameObject(searchGameObject, out searchGameObject);
// Try Get AI target
return searchGameObject.TryGetComponent<TargetType>(out foundTarget);
}
/// <summary> Same with <see cref="GameObject.TryGetComponent(System.Type, out Component)"/> except it reflects the method to desired <see cref="GameObject"/> via <see cref="EventReflector"/> if there is any </summary>
public static bool TryGetComponentByEventReflector(Type targetType, GameObject searchGameObject, out Component foundTarget)
{
// Try get the reflected GameObject otherwise return same
TryGetReflectedGameObject(searchGameObject, out searchGameObject);
// Try Get AI target
return searchGameObject.TryGetComponent(targetType, out foundTarget);
}
}
using System;
using System.IO;
using Cysharp.Text;
using Newtonsoft.Json;
using UnityEngine;
// Save&Load Copyright belongs to: https://github.com/shapedbyrainstudios/save-load-system - I upgraded the code
public static class IOUtils
{
public const string encryptionWord = "Lightout";
public const string backupExtension = ".backup";
/// <summary> Replaces '/' with <see cref="Path.DirectorySeparatorChar"/> </summary>
public static string FixPathByCorrectDirectorySeperator(string path)
{
return path.Replace('/', Path.DirectorySeparatorChar);
}
/// <inheritdoc cref="FixPathByCorrectDirectorySeperator(string)"/>
public static string FixPathByCorrectDirectorySeperator(ref string path)
=> path = FixPathByCorrectDirectorySeperator(path);
/// <summary> Uses Newtonsoft JSON to deserialize </summary>
public static bool Load<LoadObjectType>(string fullPathWithExtension, out LoadObjectType loadedData, bool useDecryption = false, bool allowRestoreFromBackup = true)
{
loadedData = default;
FixPathByCorrectDirectorySeperator(ref fullPathWithExtension);
// Try to load backup if file does not exists
if (!File.Exists(fullPathWithExtension))
{
Debug.LogError($"Failed to load file at path: {fullPathWithExtension} File does not exists");
return allowRestoreFromBackup && TrySaveRollbackAsMainFile(fullPathWithExtension) && Load<LoadObjectType>(fullPathWithExtension, out loadedData, useDecryption, false);
}
// load the serialized data from the file
try
{
string dataToLoad = "";
using (var stream = new FileStream(fullPathWithExtension, FileMode.Open))
{
using var reader = new StreamReader(stream);
dataToLoad = reader.ReadToEnd();
}
if (useDecryption)
dataToLoad = EncryptDecrypt(dataToLoad);
loadedData = JsonConvert.DeserializeObject<LoadObjectType>(dataToLoad);
return true;
}
// Try to load backup if any exists
catch (Exception e)
{
if (allowRestoreFromBackup)
{
Debug.LogWarning($"Failed to load file at path: {fullPathWithExtension} Attempting to rollback. Error occured: {e}");
if (TrySaveRollbackAsMainFile(fullPathWithExtension) && Load<LoadObjectType>(fullPathWithExtension, out loadedData, useDecryption, false))
return true;
}
// if we hit here, one possibility is that the backup file is also corrupt
Debug.LogError($"Failed to load file at path: {fullPathWithExtension} and backup did not work. Maybe the file is corrupted. Error occured: {e}");
}
return false;
}
/// <summary> Uses Newtonsoft JSON to serialize </summary>
public static void Save<SaveObjectType>(SaveObjectType data, string fullPathWithExtension, bool useEncryption = false, bool createBackup = true)
{
FixPathByCorrectDirectorySeperator(ref fullPathWithExtension);
try
{
Directory.CreateDirectory(Path.GetDirectoryName(fullPathWithExtension));
string dataToStore = JsonConvert.SerializeObject(data);
if (useEncryption)
dataToStore = EncryptDecrypt(dataToStore);
// write the serialized data to the file
using (var stream = new FileStream(fullPathWithExtension, FileMode.Create))
{
using var writer = new StreamWriter(stream);
writer.Write(dataToStore);
}
// verify the newly saved file can be loaded successfully
// if the data can be verified, back it up
if (Load<SaveObjectType>(fullPathWithExtension, out _, useEncryption, false) && createBackup)
File.Copy(fullPathWithExtension, ZString.Concat(fullPathWithExtension, backupExtension), true);
else
throw new Exception($"Save file could not be verified and backup could not be created at path: {fullPathWithExtension}");
}
catch (Exception e)
{
Debug.LogError($"Error occured when trying to save data to file at path: {fullPathWithExtension} Error occured: {e}");
}
}
public static void Delete(string fullPathWithExtension, bool deleteBackup = true)
{
if (!File.Exists(fullPathWithExtension))
{
Debug.LogError($"Failed to delete file at path: {fullPathWithExtension} File does not exists");
return;
}
if (deleteBackup)
File.Delete(ZString.Concat(fullPathWithExtension, backupExtension));
File.Delete(fullPathWithExtension);
}
/// <summary> Simple implementation of XOR encryption </summary>
private static string EncryptDecrypt(string data)
{
using var stringBuilder = ZString.CreateStringBuilder();
stringBuilder.TryGrow(data.Length);
for (int i = 0; i < data.Length; i++)
stringBuilder.Append((char)(data[i] ^ encryptionWord[i % encryptionWord.Length]));
return stringBuilder.ToString();
}
private static bool TrySaveRollbackAsMainFile(string fullPathWithExtension)
{
string backupFilePath = ZString.Concat(fullPathWithExtension, backupExtension);
FixPathByCorrectDirectorySeperator(ref backupFilePath);
try
{
if (!File.Exists(backupFilePath))
{
Debug.LogError("Tried to Rollback but no backup file exists to roll back to.");
return false;
}
File.Copy(backupFilePath, fullPathWithExtension, true);
Debug.Log($"Saved backup as main file to: {fullPathWithExtension}");
return true;
}
catch (Exception e)
{
Debug.LogError($"Failed to Rollback when trying to roll back to backup file at: {backupFilePath} Error Occured: {e}");
return false;
}
}
}
using System;
using UnityEngine;
public static class LuckUtils
{
public static LuckType Generate()
{
var generatedLuck = LuckType.VeryCommon;
var generatedValue = UnityEngine.Random.value;
// Check the possibilites.
// For example:
// (generatedValue = 0,7) and (posibility = 0.9)
// In that case, (inner posibility = 0,9 - (0,9 * 0,25)) which equals (inner posibility = 0,675)
for (int i = (int)LuckType.Impossible; i >= (int)LuckType.VeryCommon; i >>= 1)
{
// Get rid of division by zero error
if (i == 0)
continue;
if (Enum.IsDefined(typeof(LuckType), i))
{
var possibility = (1f / Math.Abs(i));
var selectionSize = 0.25f;
var innerPosibility = Mathf.Clamp01(possibility - (possibility * selectionSize));
var isValid = (generatedValue <= possibility) && (generatedValue >= innerPosibility);
if (isValid)
{
generatedLuck = (LuckType)i;
break;
}
}
}
return generatedLuck;
}
}
using System;
using UnityEngine;
public static class MathfUtils
{
/// <summary> Returns the angle in 2PI radians whose Tan is y/x. Same as <see cref="Mathf.Atan2(float, float)"/> but returns in 2PI radians (0-360 degree) instead of PI radians (-180~180 degree) </summary>
public static float Atan2_360(float y, float x)
{
var radian = MathF.Atan2(y, x);
// Same as adding 360 degree to a angle in degrees
if (radian < 0f)
radian += (Mathf.PI * 2);
return radian;
}
public static float InvertAngle_360(float angleInDegrees)
{
return (angleInDegrees + (180 * Math.Sign(angleInDegrees))) % 360;
}
public static float InvertAngle(float angleInRadians)
{
return (angleInRadians + (Mathf.PI * Math.Sign(angleInRadians))) % (Mathf.PI * 2);
}
}
using System;
using UnityEngine;
public static class VectorUtils
{
private static readonly System.Random randomizer = new();
public static Vector3 RandomRange(Vector3 minInclusive, Vector3 maxExclusive)
{
var x = randomizer.NextFloat(minInclusive.x, maxExclusive.x);
var y = randomizer.NextFloat(minInclusive.y, maxExclusive.y);
var z = randomizer.NextFloat(minInclusive.z, maxExclusive.z);
return new Vector3(x, y, z);
}
public static Vector2 RadianToNormalizedVector(float angleInRadians)
{
return new Vector2(MathF.Cos(angleInRadians), MathF.Sin(angleInRadians));
}
public static Vector3 RadianToVector(float angleInRadians, Vector3 axisToRotateAround)
=> DegreeToVector(angleInRadians * Mathf.Rad2Deg, axisToRotateAround);
public static Vector2 DegreeToNormalizedVector(float angleInDegrees)
=> RadianToNormalizedVector(angleInDegrees * Mathf.Deg2Rad);
public static Vector3 DegreeToVector(float angleInDegrees, Vector3 axisToRotateAround)
=> Quaternion.AngleAxis(angleInDegrees, axisToRotateAround).eulerAngles;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment