Ok, I'm confused a bit. I studied UnityEvent and Messaging System by this tutorial. But I have one question, that I cannot understand. How can I pass arguments to Invoking function?
For example(I have EventManager like in tutorial):
void OnEnable() {
EventManager.StartListening ("OnPlayerTeleport", TeleportPlayer);
}
void OnDisable() {
EventManager.StopListening ("OnPlayerTeleport", TeleportPlayer);
}
void TeleportPlayer () {
float yPos = transform.position.y;
yPos += 20.0f;
transform.position = new Vector3 (transform.position.x, yPos, transform.position.z);
}
And I have trigger:
void Update () {
if (Input.GetButtonDown ("Teleport")) {
EventManager.TriggerEvent ("OnPlayerTeleport");
}
}
But, what if I want to pass 'height' value to function 'TeleportPlayer':
void TeleportPlayer (float h) {
float yPos = transform.position.y;
yPos += h;
transform.position = new Vector3 (transform.position.x, yPos, transform.position.z);
}
How can I do this?
Use C# delegate/Action instead of Unity's UnityEvent. It is faster than Unity's event. I ported it few days ago. You just need to modify that a little bit to get what you are looking for.
1.Make Action take float by changing all Action declaration to Action<float> which means that it will allow functions with float parameter.
2.Now, make the TriggerEvent function take a float parameter. by changing
public static void TriggerEvent(string eventName)
to
public static void TriggerEvent(string eventName, float h)
Here is the new EventManager script.
using UnityEngine;
using System.Collections.Generic;
using System;
public class EventManager : MonoBehaviour
{
private Dictionary<string, Action<float>> eventDictionary;
private static EventManager eventManager;
public static EventManager instance
{
get
{
if (!eventManager)
{
eventManager = FindObjectOfType(typeof(EventManager)) as EventManager;
if (!eventManager)
{
Debug.LogError("There needs to be one active EventManger script on a GameObject in your scene.");
}
else
{
eventManager.Init();
}
}
return eventManager;
}
}
void Init()
{
if (eventDictionary == null)
{
eventDictionary = new Dictionary<string, Action<float>>();
}
}
public static void StartListening(string eventName, Action<float> listener)
{
Action<float> thisEvent;
if (instance.eventDictionary.TryGetValue(eventName, out thisEvent))
{
thisEvent += listener;
}
else
{
thisEvent += listener;
instance.eventDictionary.Add(eventName, thisEvent);
}
}
public static void StopListening(string eventName, Action<float> listener)
{
if (eventManager == null) return;
Action<float> thisEvent;
if (instance.eventDictionary.TryGetValue(eventName, out thisEvent))
{
thisEvent -= listener;
}
}
public static void TriggerEvent(string eventName, float h)
{
Action<float> thisEvent = null;
if (instance.eventDictionary.TryGetValue(eventName, out thisEvent))
{
thisEvent.Invoke(h);
}
}
}
Test:
public class Test : MonoBehaviour
{
void OnEnable()
{
EventManager.StartListening("OnPlayerTeleport", TeleportPlayer);
}
void OnDisable()
{
EventManager.StopListening("OnPlayerTeleport", TeleportPlayer);
}
void Update()
{
if (Input.GetButtonDown("Teleport"))
{
EventManager.TriggerEvent("OnPlayerTeleport", 5);
}
}
void TeleportPlayer(float h)
{
float yPos = transform.position.y;
yPos += h;
transform.position = new Vector3(transform.position.x, yPos, transform.position.z);
}
}
Create a typed subclass of unityevent to take a parameter that is passed through to listeners and invoke that instead.
Documentation with an example using integer data is here
https://docs.unity3d.com/ScriptReference/Events.UnityEvent_1.html
Related
I have been repeatedly getting an error, when trying to add a script to a gameObject. I have so far tried changing the MonoBehavior class name to what I had as the scripts name, and checked my code for errors, though none are shown in the terminal. Here`s my code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace LE
{
public class InputHandler : MonoBehaviour
{
public float horizontal;
public float vertical;
public float moveAmountl;
public float mouseX;
public float mouseY
PlayerControls inputActions;
Vector2 movementInput;
Vector2 cameraInput
public void private void OnEnable() {
{
if (inputActions == null)
{
inputActions = new PlayerControls();
inputActions.PlayerMovement.Movement.performed += inputActions => movementInput = inputActions.ReadValue<Vector2>();
inputActions.PlayerMovement.Camera.performed += i => cameraInput = i.ReadValue<Vector2>();
}
inputActions.Enable();
}
private void private void OnDisable()
{
inputActions.Disable();
}
}
public void TickInput(float delta)
{
MoveInput(delta);
}
private void MoveInput(float delta)
{
horizontal = movementInput.x;
vertical = movementInput.y;
moveAmountv= Mathf.Clamp01(Mathf.Abs(horizontal) + Mathf.abs(vertical));
mouseX = cameraInput.x;
mouseY = cameraInput.y;
}
}
}
It could be this (note typed twice private and public void):
public void private void OnEnable() {
private void private void OnDisable()
{
inputActions.Disable();
}
should be
private void OnEnable() {
private void OnDisable()
{
inputActions.Disable();
}
This is the code for detecting images with ARFoundation and enabling objects depending on the image the device is scanning
[SerializeField]
public GameObject[] placeablePrefabs;
private Dictionary<string, GameObject> spawnedPrefabs = new Dictionary<string, GameObject>();
private ARTrackedImageManager trackedImageManager;
private JSONObject warnings;
public void Awake()
{
trackedImageManager = GetComponent<ARTrackedImageManager>();
foreach(GameObject prefab in placeablePrefabs)
{
GameObject newPrefab = Instantiate(prefab, Vector3.zero, Quaternion.identity);
newPrefab.name = prefab.name;
spawnedPrefabs.Add(prefab.name, newPrefab);
}
}
private void OnEnable()
{
trackedImageManager.trackedImagesChanged += ImageChanged;
}
private void OnDisable()
{
trackedImageManager.trackedImagesChanged -= ImageChanged;
}
private void ImageChanged(ARTrackedImagesChangedEventArgs eventArgs)
{
foreach(ARTrackedImage trackedImage in eventArgs.added)
{
UpdateImage(trackedImage);
}
foreach (ARTrackedImage trackedImage in eventArgs.updated)
{
UpdateImage(trackedImage);
}
foreach (ARTrackedImage trackedImage in eventArgs.removed)
{
spawnedPrefabs[trackedImage.name].SetActive(false);
}
}
private void UpdateImage(ARTrackedImage trackedImage)
{
string name = trackedImage.referenceImage.name;
Vector3 position = trackedImage.transform.position;
GameObject prefab = spawnedPrefabs[name];
prefab.transform.position = position;
prefab.SetActive(true);
foreach(GameObject go in spawnedPrefabs.Values)
{
if(go.name != name)
{
go.SetActive(false);
}
}
}
It is working as expected when I am switching from one image to another, but, if I switch back, nothing happens, the objects wont switch visible state again. They do only on the first Switch.
Problem was that eventArgs.removed did not get called. Do not know why.
I use 3.1.3 ARFoundation and ARCore versions.
Anyway, I managed to find a solution. I will post the whole code in case someone wants the whole think.
It goes like this:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using System;
using UnityEngine.UI;
using UnityEngine.XR.ARSubsystems;
public class ImageRecognitionExample : MonoBehaviour
{
private ARTrackedImageManager m_ImageManager;
[SerializeField]
private GameObject welcomePanel;
[SerializeField]
private Button dismissButton;
[SerializeField]
private Text imageTrackedText;
[SerializeField]
private GameObject[] arObjectsToPlace;
[SerializeField]
private Vector3 scaleFactor = new Vector3(0.1f, 0.1f, 0.1f);
private ARTrackedImageManager m_TrackedImageManager;
private Dictionary<string, GameObject> arObjects = new Dictionary<string, GameObject>();
String currentActiveQR;
void Awake()
{
dismissButton.onClick.AddListener(Dismiss);
m_ImageManager = GetComponent<ARTrackedImageManager>();
foreach (GameObject arObject in arObjectsToPlace)
{
GameObject newARObject = Instantiate(arObject, Vector3.zero, Quaternion.identity);
newARObject.name = arObject.name;
arObjects.Add(arObject.name, newARObject);
}
}
void OnEnable()
{
m_ImageManager.trackedImagesChanged += OnTrackedImagesChanged;
}
void OnDisable()
{
m_ImageManager.trackedImagesChanged -= OnTrackedImagesChanged;
}
void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs)
{
foreach (ARTrackedImage trackedImage in eventArgs.added)
{
currentActiveQR = trackedImage.referenceImage.name;
UpdateARImage(trackedImage, currentActiveQR);
}
foreach (ARTrackedImage trackedImage in eventArgs.updated)
{
if (trackedImage.trackingState == TrackingState.Tracking)
{
currentActiveQR = trackedImage.referenceImage.name;
imageTrackedText.text = currentActiveQR;
AssignGameObject(currentActiveQR, trackedImage.transform.position);
}
else
{
if (currentActiveQR != trackedImage.referenceImage.name)
{
ReAssignGameObject(trackedImage.referenceImage.name, trackedImage.transform.position);
}
}
}
}
private void UpdateARImage(ARTrackedImage trackedImage, String name)
{
imageTrackedText.text = trackedImage.referenceImage.name;
AssignGameObject(name, trackedImage.transform.position);
}
void AssignGameObject(string name, Vector3 newPosition)
{
if (arObjectsToPlace != null)
{
GameObject goARObject = arObjects[name];
goARObject.SetActive(true);
goARObject.transform.position = newPosition;
goARObject.transform.localScale = scaleFactor;
foreach (GameObject go in arObjects.Values)
{
//Debug.Log($"Go in arObjects.Values: {go.name}");
if (go.name != name)
{
go.SetActive(false);
}
}
}
}
void ReAssignGameObject(string name, Vector3 newPosition)
{
if (arObjectsToPlace != null)
{
GameObject goARObject = arObjects[name];
goARObject.SetActive(true);
goARObject.transform.position = newPosition;
goARObject.transform.localScale = scaleFactor;
foreach (GameObject go in arObjects.Values)
{
if (go.name == name)
{
go.SetActive(false);
}
}
}
}
private void Dismiss() => welcomePanel.SetActive(false);
}
I followed this video on how to create health bars automatically for units.
This is my HealthBar class.
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class HealthBar : MonoBehaviour
{
#region SerializeFields
[SerializeField] private Image foregroundImage;
[SerializeField] private float updateSpeedInSec = 0.5f;
[SerializeField] private float positionOffset = 1f;
#endregion
#region NonSerializeFields
private Health health;
#endregion
public void SetHealth(Health healthToSet)
{
health = healthToSet;
healthToSet.OnHealthPctChanged += HandleHealthChanged;
}
private void HandleHealthChanged(float pct)
{
StartCoroutine(ChangeToPct(pct));
}
private IEnumerator ChangeToPct(float pct)
{
float preChangedPct = foregroundImage.fillAmount;
float elapsedTime = 0f;
while (elapsedTime < updateSpeedInSec)
{
elapsedTime += Time.deltaTime;
foregroundImage.fillAmount = Mathf.Lerp(preChangedPct, pct, elapsedTime / updateSpeedInSec);
yield return null;
}
foregroundImage.fillAmount = pct;
}
private void LateUpdate()
{
var worldToScreenPoint = Camera.main.WorldToScreenPoint(health.transform.position + (Vector3) Vector2.up * positionOffset);
transform.position = worldToScreenPoint;
}
private void OnDestroy()
{
health.OnHealthPctChanged -= HandleHealthChanged;
}
}
So this is my HealthBarController class, which is where the SetHealth method is called, put on a canvas.
using System.Collections.Generic;
using UnityEngine;
public class HealthBarController : MonoBehaviour
{
#region SerializeFields
[SerializeField] private HealthBar healthBar;
#endregion
#region NonSerializeFields
private Dictionary<Health, HealthBar> healthBars = new Dictionary<Health, HealthBar>();
#endregion
private void Awake()
{
Health.OnHealthAdded += AddHealthBar;
Health.OnHealthRemoved += RemoveHealthBar;
}
private void AddHealthBar(Health health)
{
if (healthBars.ContainsKey(health)) return;
var newHealthBar = Instantiate(healthBar, transform);
healthBars.Add(health, newHealthBar);
healthBar.SetHealth(health);
}
private void RemoveHealthBar(Health health)
{
if (!healthBars.ContainsKey(health)) return;
Destroy(healthBars[health].gameObject);
healthBars.Remove(health);
}
}
And this is my Health class on a player character.
using System;
using UnityEngine;
public class Health : MonoBehaviour, IDamageable
{
#region SerializeFields
[SerializeField] protected int maxHealth = 100;
public static event Action<Health> OnHealthAdded = delegate { };
public static event Action<Health> OnHealthRemoved = delegate { };
public event Action<float> OnHealthPctChanged = delegate { };
#endregion
#region NonSerializeFields
protected int currentHealth;
#endregion
private void OnEnable()
{
currentHealth = maxHealth;
OnHealthAdded(this);
}
public void TakeDamage(int damage)
{
currentHealth -= damage;
float currentHealthPct = (float) currentHealth / maxHealth;
OnHealthPctChanged(currentHealthPct);
if (currentHealth <= 0)
{
Die();
}
}
protected void Die()
{
OnHealthRemoved(this);
Destroy(gameObject);
}
}
The problem I'm having is in the LateUpdate method, the health field is null, even though in the SetHealth method, it was set properly.
In the AddHealthBar method of your HealthBarController.cs script you are assigning the Health class of your character to the Prefab healthBar rather than the newly created Health instance newHealthBar.
Simply replace healthBar.SetHealth(health); with newHealthBar.SetHealth(health); (line 30).
I've just updated to the new Input System from 2.7 to 2.8.
How the new Input System works is you create an input actions asset by going to Create-> Input Actions
.
This creates an asset where actions can be mapped to keys. One then create a C# script from this asset and use it in their code. Which is what I did. I called the Asset MyInput.inputactions and the C# script is MyInput.cs
When you use the generated C# script this way you need to reference the asset in your script. However, after the update, it seems this is impossible to do from the editor. When I define a public MyInput variable in my class, like so:
public class ShapeMover: MonoBehaviour
{
public MyInput controls;
private float _lastFallTime;
private float _fallSpeed;
private ShapeSpawner _spawn;
private GameObject _shapeToMove;
private Transform _shapeToMoveTransform;
private bool _isGameOver;
private const float _leftRotationAngle = (float) -1.57079633;
private const float _rightRotationAngle = (float) 1.57079633;
}
It isn't exposed in the inspector:
And I get an obvious NullReferenceExceptionerror when I try to access the controls variable.
Am I doing something wrong?
How can I reference the asset from the inspector? I have tried adding [SerializeField] to the public declaration, it didn't help.
I was following this video and it worked fine until I updated to a newer Input System version.
For reference, this is the full ShapeMover class:
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
namespace _Scripts
{
public class ShapeMover : MonoBehaviour
{
[SerializeField]
public MyInput controls;
private float _lastFallTime;
private float _fallSpeed;
private ShapeSpawner _spawn;
private GameObject _shapeToMove;
private Transform _shapeToMoveTransform;
private bool _isGameOver;
private const float _leftRotationAngle = (float) -1.57079633;
private const float _rightRotationAngle = (float) 1.57079633;
private void Awake()
{
_spawn = FindObjectOfType<ShapeSpawner>();
_lastFallTime = 0f;
_fallSpeed = GameGrid.Instance.GetFallSpeed();
_isGameOver = false;
Debug.Log("Registering controls callbacks...");
controls.Player.Movement.performed += ctx => Movement(ctx.ReadValue<Vector2>(), true);
controls.Player.Drop.performed += ctx => Drop();
controls.Menu.Reset.performed += ctx => Restart();
controls.Menu.Pause.performed += ctx => PauseToggle();
SetShapeToMove();
}
private void Restart()
{
GameGrid.Instance.ResetGame();
_isGameOver = false;
SetShapeToMove();
}
private void PauseToggle()
{
Debug.Log("Got Pause input");
var currentPauseState = GameGrid.Instance.IsPaused;
//If not paused, will pause
if (!currentPauseState)
{
// controls.Player.Movement.Disable();
// controls.Player.Drop.Disable();
// controls.Player.Menu.Disable();
// controls.Player.Disable();
GameGrid.Instance.IsPaused = true;
}
else
{
// controls.Player.Movement.Enable();
// controls.Player.Drop.Enable();
// controls.Player.Menu.Enable();
// controls.Player.Enable();
GameGrid.Instance.IsPaused = false;
}
}
private void Drop()
{
// Debug.Log("Should Drop Shape!");
bool didMove = true;
while (didMove)
{
didMove = Movement(new Vector2(0, -1), false);
}
}
private bool Movement(Vector2 direction, bool isFromInput)
{
if (isFromInput)
{
Debug.Log($"Got input {direction.ToString()}");
}
//Disable movement controls when game is over.
if (_isGameOver)
{
return false;
}
var oldPosition = _shapeToMoveTransform.position;
var oldRotation = _shapeToMoveTransform.rotation;
// Transform[] children = _shapeToMoveTransform.Cast<Transform>().ToArray();
var didMove = true;
var didEndMovement = false;
GameGrid.Instance.RemoveShapeFromGrid(_shapeToMoveTransform);
if (direction.x < 0)
{
didMove = MoveLeft();
}
else if (direction.x > 0)
{
didMove = MoveRight();
}
else if (direction.y > 0)
{
didMove = RotateLeft();
}
else if (direction.y < 0)
{
didMove = MoveDown();
if (!didMove)
{
didEndMovement = true;
}
}
//If Shape didn't move, restore previous position.
if (!didMove)
{
_shapeToMoveTransform.position = oldPosition;
_shapeToMoveTransform.rotation = oldRotation;
}
GameGrid.Instance.AddShapeToGrid(_shapeToMoveTransform);
// Debug.Log($"Shape {_shapeToMove.name} Position after movement Did Move: {didMove.ToString()}");
// Transform[] children = _shapeToMoveTransform.Cast<Transform>().ToArray();
// var lowestChild = children.OrderBy(x => x.position.y).First();
// Debug.Log($"{lowestChild.position.ToString()}");
if (didEndMovement)
{
GameGrid.Instance.ClearRows(_shapeToMoveTransform);
_isGameOver = GameGrid.Instance.IsGameOver(_shapeToMoveTransform);
if (!_isGameOver)
{
SetShapeToMove();
}
}
return didMove;
}
private void SetShapeToMove()
{
_shapeToMove = _spawn.SpawnShape();
_shapeToMoveTransform = _shapeToMove.transform;
}
private void Update()
{
if (_isGameOver)
{
return;
}
if (GameGrid.Instance.IsPaused)
{
return;
}
var time = Time.time;
if (!(time - (_lastFallTime + _fallSpeed) > 0))
{
return;
}
Movement(new Vector2(0, -1), false);
_lastFallTime = time;
_fallSpeed = GameGrid.Instance.GetFallSpeed();
}
private bool MoveLeft()
{
_shapeToMoveTransform.position += Vector3.right;
return GameGrid.Instance.CanMove(_shapeToMoveTransform);
}
private bool MoveRight()
{
_shapeToMoveTransform.position += Vector3.left;
return GameGrid.Instance.CanMove(_shapeToMoveTransform);
}
private bool MoveDown()
{
_shapeToMoveTransform.position += Vector3.down;
return GameGrid.Instance.CanMove(_shapeToMoveTransform);
}
private bool RotateLeft()
{
_shapeToMoveTransform.Rotate(0, 0, -90);
// foreach (Transform child in _shapeToMoveTransform)
// {
// RotateTransform(child, _leftRotationAngle);
// }
return GameGrid.Instance.CanMove(_shapeToMoveTransform);
}
private void RotateTransform(Transform transformToRotate, float rotationAngleRadian)
{
var currentLocalPosition = transformToRotate.localPosition;
var currentX = currentLocalPosition.x;
var currentY = currentLocalPosition.y;
var rotatedX = currentX * Mathf.Cos(rotationAngleRadian) - currentY * Mathf.Sin(rotationAngleRadian);
var rotatedY = currentX * Mathf.Sin(rotationAngleRadian) + currentY * Mathf.Cos(rotationAngleRadian);
transformToRotate.localPosition = new Vector2(rotatedX, rotatedY);
// Debug.Log($"Position after rotation is: {transformToRotate.localPosition.ToString()}");
}
private bool RotateRight()
{
_shapeToMoveTransform.Rotate(0, 0, -90);
return GameGrid.Instance.CanMove(_shapeToMoveTransform);
}
private void OnEnable()
{
Debug.Log("Controls Enabled...");
controls.Enable();
}
// private void OnDisable()
// {
// Debug.Log("Controls Disabled...");
// controls.Disable();
// }
}
}
Just as you said, you can't reference the new generated input class anymore.
To make it works, i instantiated the class, and use the SetCallbacks method, like this :
private MyInput _inputs;
public void Awake()
{
_inputs = new MyInput();
}
Truth be told, i don't know if it's the intended way of using the input class, but it works.
EDIT :
Starting from the 2.8 preview, an interface is automatically generated. I can only recommend it, cause it's very easy to use, you just need to inherits from IYourActionsSetNameActions and add the callbacks. (Also, you have to enable / disable the actions set, but you should be able to do it in another script)
Here is a complete base example, using your naming :
public class ShapeMover : MonoBehaviour, MyInput.IPlayerActions
{
private MyInput _inputs;
public void Awake()
{
_inputs = new MyInput();
_inputs.Player.SetCallbacks(this);
}
public void OnEnable()
{
_inputs.Player.Enable();
}
public void OnDisable()
{
_inputs.Player.Disable();
}
public void OnMovement(InputAction.CallbackContext context)
{
Vector2 delta = context.ReadValue<Vector2>();
transform.position += new Vector3(delta.x, 0, delta.y);
}
public void OnDrop(InputAction.CallbackContext context)
{
//TODO
}
// ...
}
I'm started with Unity, and I need (before game is in paused) trigger an avent. Is there any way to trigger any event when the Time.timeScale is changed to 0? Something like: Time.timeScale.onBeforeChange()...
Thanks a lot.
Make the thing that changes the time scale a controller, then make that controller raise the event.
[System.Serializable]
public class BeforeTimeChangedData
{
public bool canceled;
public float oldValue;
public float newValue;
}
[System.Serializable]
public class BeforeTimeChangedEvent : UnityEvent<BeforeTimeChangedData>
{
}
//Attach this to a game object that gets loaded in your pre-load scene with the tag "Singletons"
public class TimeController : MonoBehaviour
{
void Awake()
{
DontDestroyOnLoad(gameObject);
if(BeforeTimeChanged == null)
BeforeTimeChanged = new BeforeTimeChangedEvent();
}
public BeforeTimeChangedEvent BeforeTimeChanged;
public bool ChangeTimeScale(float newValue)
{
var args = new BeforeTimeChangedData();
args.oldValue = Time.timeScale;
args.newValue = Time.timeScale;
BeforeTimeChanged.Invoke(args);
if(!args.canceled)
{
Time.timeScale = newValue;
}
return args.canceled;
}
}
Elsewhere you can change the timescale by doing
public class TimeSlower : MonoBehaviour
{
private TimeController _timeController;
public Text TimeChanged;
void Start()
{
var singletons = GameObject.FindWithTag("Singletons");
_timeController = singletons.GetComponent<TimeController>();
if(_timeController == null)
throw new System.ArgumentNullException("Could not find a TimeController on the Singletons object");
}
void Update()
{
if(Input.GetButton("SlowTime"))
{
var changed = _timeController.ChangeTimeScale(0.5f);
if(changed)
{
TimeChanged.text = "Time Changed!";
}
}
}
}
And here is another component that listens for the change and cancels the change if the change has happened too recently;
public class TimeChangeLimiter : MonoBehaviour
{
private float lastTimeChange = 0;
private TimeController _timeController;
public Text TimeChanged;
[Range(0, float.MaxValue)]
public float Cooldown;
void Start()
{
var singletons = GameObject.FindWithTag("Singletons");
_timeController = singletons.GetComponent<TimeController>();
if(_timeController == null)
throw new System.ArgumentNullException("Could not find a TimeController on the Singletons object");
_timeController.BeforeTimeChanged.AddListener(OnBeforeTimeChanged);
}
void OnDestroy()
{
_timeController.BeforeTimeChanged.RemoveListener(OnBeforeTimeChanged);
}
void OnBeforeTimeChanged(BeforeTimeChangedData args)
{
if(Time.time - lastTimeChange < Cooldown)
{
args.canceled = true;
return;
}
lastTimeChange = Time.time;
}
}