Best practices for decoupling input from other scripts in Unity - c#

I have an input script that translates touches to directions (Left, Right, Up, Down) with a magnitude (0-1) in the Update loop, and when an input is detected this script fires a UnityEvent:
public class TouchAnalogStickInput : MonoBehaviour
{
[System.Serializable]
public class AnalogStickInputEvent : UnityEvent<Direction, float> { }
[Header("Events")]
[Space]
[Tooltip("Fired when a successful swipe occurs. Event Args: Swipe Direction, A normalized input magnitude between 0 and 1.")]
public AnalogStickInputEvent OnAnalogStickInput;
void Update()
{
...
if (successfulInputDetected)
{
OnAnalogStickInput.Invoke(mInputDirection, normalizedInputMag);
}
}
}
I'm subscribing to this OnAnalogStickInput event from the Unity Inspector to call my CharacterController2D.Move method, which takes a Direction and a magnitude:
public class CharacterController2D : MonoBehaviour
{
public void Move(Direction movementDirection, float normalizedInputMagnitude)
{
// Move the character.
}
}
Then I have another GameObject that I wish to rotate using the same input script. so I've attached TouchAnalogStickInput and SnapRotator to this GameObject, subscribing the OnAnalogStickInput event to call SnapRotator.Rotate:
public class SnapRotator : MonoBehaviour
{
public void Rotate(Direction movementDirection, float normalizedInputMagnitude)
{
// Rotate object.
}
}
At this point I've come to realize I'm no longer in charge of which game loops these methods are called from, e.g. I should be detecting input in Update, using this input in FixedUpdate for movement, and perhaps in my case I'd like to do the rotation last in LateUpdate. Instead both CharacterController2D.Move and SnapRotator.Rotate are being fired from the Update loop that the input code runs in.
The only other option I can think of is perhaps refactoring the Input script's code into a method call. Then having CharacterController2D and SnapRotator call this method in the Update loop, executing movement/rotation in the FixedUpdate or LateUpdate loop as required, e.g.:
public class CharacterController2D : MonoBehaviour
{
public TouchAnalogStickInput Input;
private var mMovementInfo;
void Update()
{
// Contains a Direction and a Normalized Input Magnitude
mMovementInfo = Input.DetectInput();
}
void FixedUpdate()
{
if (mMovementInfo == Moved)
// Move the character.
}
}
My question is: What's the best practice for decoupling scripts like these in Unity? Or am I taking re-usability too far/being overly afraid of coupling sub classes/components in game development?
Solution
In case it's helpful for anyone else here's my final solution, in semi-sudocode, credit to Ruzihm:
Utility class to store information about a detected input:
public class InputInfo
{
public Direction Direction { get; set; } = Direction.None;
public float NormalizedMagnitude { get; set; } = 0f;
public TouchPhase? CurrentTouchPhase { get; set; } = null;
public InputInfo(Direction direction, float normalizedMagnitude, TouchPhase currentTouchPhase)
{
Direction = direction;
NormalizedMagnitude = normalizedMagnitude;
CurrentTouchPhase = currentTouchPhase;
}
public InputInfo()
{
}
}
Input class:
public class TouchAnalogStickInput : MonoBehaviour
{
[System.Serializable]
public class AnalogStickInputEvent : UnityEvent<InputInfo> { }
[Header("Events")]
[Space]
[Tooltip("Fired from the Update loop when virtual stick movement occurs. Event Args: Swipe Direction, A normalized input magnitude between 0 and 1.")]
public AnalogStickInputEvent OnUpdateOnAnalogStickInput;
[Tooltip("Fired from the FixedUpdate loop when virtual stick movement occurs. Event Args: Swipe Direction, A normalized input magnitude between 0 and 1.")]
public AnalogStickInputEvent OnFixedUpdateOnAnalogStickInput;
[Tooltip("Fired from the LateUpdate loop when virtual stick movement occurs. Event Args: Swipe Direction, A normalized input magnitude between 0 and 1.")]
public AnalogStickInputEvent OnLateUpdateOnAnalogStickInput;
private bool mInputFlag;
private InputInfo mInputInfo;
void Update()
{
// Important - No input until proven otherwise, reset all members.
mInputFlag = false;
mInputInfo = new InputInfo();
// Logic to detect input
...
if (inputDetected)
{
mInputInfo.Direction = direction;
mInputInfo.NormalizedMagnitude = magnitude;
mInputInfo.CurrentTouchPhase = touch.phase;
// Now that the Input Info has been fully populated set the input detection flag.
mInputFlag = true;
// Fire Input Event to listeners
OnUpdateOnAnalogStickInput.Invoke(mInputInfo);
}
void FixedUpdate()
{
if (mInputFlag)
{
OnFixedUpdateOnAnalogStickInput.Invoke(mInputInfo);
}
}
void LateUpdate()
{
OnLateUpdateOnAnalogStickInput.Invoke(mInputInfo);
}
}
}
Character controller that subscribes to the OnFixedUpdateOnAnalogStickInput event.
public class CharacterController2D : MonoBehaviour
{
public void Move(InputInfo inputInfo)
{
// Use inputInfo to decide how to move.
}
}
Rotation class is much the same but subscribes to the OnLateUpdateOnAnalogStickInput event.

Here is one alternative, which mostly requires changes in your TouchAnalogStickInput class.
Set input state flags in Update and fire any relevant Events. Only this time, you fire an "OnUpdate" event (which in your specific case would not have anything registered to it):
void Update()
{
...
inputFlag_AnalogStickInput = false;
...
if (successfulInputDetected)
{
inputFlag_AnalogStickInput = true;
OnUpdateOnAnalogStickInput.Invoke(mInputDirection, normalizedInputMag);
}
}
And in TouchAnalogStickInput.FixedUpdate, call OnFixedUpdateOnAnalogStickInput, which would be registered with your Move
void FixedUpdate()
{
...
if (inputFlag_AnalogStickInput)
{
OnFixedUpdateOnAnalogStickInput.Invoke(mInputDirection, normalizedInputMag);
}
}
And so on with LateUpdate, which fires an event that your Rotate is registered with.
void LateUpdate()
{
...
if (inputFlag_AnalogStickInput)
{
OnLateUpdateOnAnalogStickInput.Invoke(mInputDirection, normalizedInputMag);
}
}
These OnFixedUpdateOn... events, of course, fire on every FixedUpdate where that flag is true. For most cases--including for Move--that is probably appropriate, but in other situations that may not be desirable. So, you can add additional events which fire on only the first FixedUpdate occurrence after an update. e.g.:
void Update()
{
...
firstFixedUpdateAfterUpdate = true;
inputFlag_AnalogStickInput = false;
...
if (successfulInputDetected)
{
inputFlag_AnalogStickInput = true;
OnUpdateOnAnalogStickInput.Invoke(mInputDirection, normalizedInputMag);
}
}
void FixedUpdate()
{
...
if (inputFlag_AnalogStickInput)
{
OnFixedUpdateOnAnalogStickInput.Invoke(mInputDirection, normalizedInputMag);
}
if (inputFlag_AnalogStickInput && firstFixedUpdateAfterUpdate)
{
OnFirstFixedUpdateOnAnalogStickInput.Invoke(mInputDirection, normalizedInputMag);
}
...
firstFixedUpdateAfterUpdate = false;
}
Hopefully that makes sense.

You may wish to look into customising Script Execution Order and having your gesture processor run before everything else.
Hope that helps. =)

Related

Raycast hits 2 objects at the same time

I have recently developed in unity and I have a problem with using the raycast.
I created 3 scripts:
Interactor: Hooked to the player, it manages all the raycast features
InteractionObject: Hooked to the objects that need to animate
InteractionRaycast: Hooked to objects that need to be destroyed
Everything works, the only problem is that when the animation of the gameobject takes place (In my case the gameobject was a pillow), the other gameobject (It is under the pillow) is destroyed at the same time as the animation begins.
My goal is to first move the pillow and then click on the gameobject to be destroyed, what can I do?
Thank you in advance for your help
Interactor.cs
public class Interactor : MonoBehaviour
{
[SerializeField]
private float _interctRange;
private InteractionObject _interactObject;
private InteractionRaycast _interactionRaycast;
private Camera _camera;
private RaycastHit _hit;
// Start is called before the first frame update
void Start()
{
_camera = Camera.main;
}
// Update is called once per frame
void Update()
{
if (Input.GetButton("Fire1"))
{
Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward, out _hit, _interctRange);
if (_hit.transform)
{
_interactObject = _hit.transform.GetComponent<InteractionObject>();
}
if (_interactObject)
{
_interactObject.PerfomAction();
}
}
}
}
InteractionObject.cs
public class InteractionObject : MonoBehaviour
{
[SerializeField]
private Vector3 _openPosition, _closePosition;
[SerializeField]
private float _animationTime;
private Hashtable _iTweenArgs;
[SerializeField]
public bool _isOpen;
// Start is called before the first frame update
void Start()
{
_iTweenArgs = iTween.Hash();
_iTweenArgs.Add("position", _openPosition);
_iTweenArgs.Add("time", _animationTime);
_iTweenArgs.Add("islocal", true);
}
public void PerfomAction()
{
if (Input.GetButton("Fire1"))
{
if (_isOpen)
{
_iTweenArgs["position"] = _closePosition;
}
else
{
_iTweenArgs["position"] = _openPosition;
}
_isOpen = !_isOpen;
iTween.MoveTo(gameObject, _iTweenArgs);
}
}
}
InteractionRaycast.cs
public class InteractionRaycast : MonoBehaviour
{
[SerializeField]
private float _range;
Ray _myRay;
RaycastHit _hit;
// Update is called once per frame
void Update()
{
if (Input.GetButton("Fire1"))
{
Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward, out _hit, _range);
if (_hit.transform)
{
Destroy(gameObject);
}
}
}
}
Tip: Use RaycastAll() and filter out the objects you want based on conditions.
It might help you with your problem, although you first should pay attention to #derHugo answer. It points out many aspects that you will want to improve in your code.
Your InteractionRaycast will destroy this own gameObject it is attaced to completely regardless of what exactly you are hitting.
You either want to additionally check like e.g.
if (Input.GetButton("Fire1"))
{
var ray = _camera.ViewportPointToRay(new Vector3(0.5f, 0.5f));
if(Physics.Raycast(ray, out var hit, _interctRange))
{
// Is this actually the object that was hit?
if(hit.transform == transform)
{
Destroy(gameObject);
}
}
}
Or - and in general I would do that - instead of having such a component on each and every object you can interact with and shooting hundreds of redundant raycasts, I would rather have a component on your player object, shoot one single raycast and interact with whatever you hit.
Both your target objects can have a common interface
public interface IInteractionObject
{
void PerfomAction();
}
meaning both types need to implement a method called PerformAction without parameters.
And rather interact directly with that in
public class Interactor : MonoBehaviour
{
[SerializeField]
private float _interctRange;
private Camera _camera;
// Start is called before the first frame update
void Start()
{
_camera = Camera.main;
}
// Update is called once per frame
void Update()
{
if (Input.GetButton("Fire1"))
{
var ray = _camera.ViewportPointToRay(new Vector3(0.5f, 0.5f));
// was something hit at all? => Check the API and return values of methods!
if(Physics.Raycast(ray, out var hit, _interctRange))
{
// Did we hit an IInteractionObject
if(hit.transform.TryGetComponent<IInteractionObject>(out var interactable))
{
// This class has no idea what exactly it is interacting with and doesn't need to know
interactable.PerfomAction();
}
}
}
}
}
and then you have different implementations:
public class AnimatedInteractionObject : MonoBehaviour, IInteractionObject
{
[SerializeField] private Vector3 _openPosition;
[SerializeField] private Vector3 _closePosition;
[SerializeField] private float _animationTime;
[SerializeField] public bool _isOpen;
private Hashtable _iTweenArgs;
private void Start()
{
_iTweenArgs = iTween.Hash();
_iTweenArgs.Add("position", _openPosition);
_iTweenArgs.Add("time", _animationTime);
_iTweenArgs.Add("islocal", true);
}
public void PerfomAction()
{
_isOpen = !_isOpen;
// use ternary makes it easier to read
_iTweenArgs["position"] = _isOpen ? _openPosition : _closePosition;
iTween.MoveTo(gameObject, _iTweenArgs);
}
}
and
public class DestroyInteractionObject : MonoBehaviour, IInteractionObject
{
public void PerfomAction()
{
// This is only called by the Interactor
// it already does a key and raycast check so no need to do that here
Destroy(gameObject);
}
}

How to Make power ups reusable in unity's karting microgame?

I'm modifying unity's karting microgame and I wanted to make the speed-pads able to be used more than once because the way they are programmed makes it so that they can only be used up until the MaxTime "x" amount of seconds is reached. The player can even pick up the power-up after the MaxTime is reached but it doesn't do anything...
I strongly suggest you download the karting microgame to have a better view of the issue but here is the code for the power-up:
using KartGame.KartSystems;
using UnityEngine;
using UnityEngine.Events;
public class ArcadeKartPowerup : MonoBehaviour {
public ArcadeKart.StatPowerup boostStats = new ArcadeKart.StatPowerup
{
MaxTime = 5f
};
public bool isCoolingDown { get; private set; }
public float lastActivatedTimestamp { get; private set; }
public float cooldown = 5f;
public bool disableGameObjectWhenActivated;
public UnityEvent onPowerupActivated;
public UnityEvent onPowerupFinishCooldown;
private void Awake()
{
lastActivatedTimestamp = -9999f;
}
private void Update()
{
if (isCoolingDown)
{
if (Time.time - lastActivatedTimestamp > cooldown)
{
//finished cooldown!
isCoolingDown = false;
onPowerupFinishCooldown.Invoke();
}
}
}
private void OnTriggerEnter(Collider other)
{
if (isCoolingDown) return;
var rb = other.attachedRigidbody;
if (rb) {
var kart = rb.GetComponent<ArcadeKart>();
if (kart)
{
lastActivatedTimestamp = Time.time;
kart.AddPowerup(this.boostStats);
onPowerupActivated.Invoke();
isCoolingDown = true;
if (disableGameObjectWhenActivated) this.gameObject.SetActive(false);
}
}
}
}
I'm did not manage to place the kart code here because it goes over the character limit, but you can easily check it out by downloading the karting microgame in Unity Hub! In this context, the Kart's script serves the function of provinding the stats that can be boosted.
The idea that I had that might (or not) work is to make it so that every time the "isCollingDown" returns just add another variable that controls how long the effect lasts to the MaxTime variable, something like "MaxTime = MaxTime + EffectDuration." But I don't know how to implement this...
I'm sorry if there is a very obvious answer to this issue but I really don't understand a lot about scripts!
It actually already has a function build in for what you want to achive so you do not have to code anything.
You see the property disableGameObjectWhenActivated. This one should be set true right now. Just set it false and it will have a cooldown instead of beeing disabled afterwards. Now if you want to get rid of the cooldown I would just set the cooldown to 0.

Unity 3d pass bool variable between two objects

how i can pass a simple boolean variable between two different object?
I can try this but didn't work...
First script:
public class CollisionController : MonoBehaviour
{
public PlayerMovement movement;
public bool active = false;
private void OnCollisionEnter(Collision collision)
{
if(collision.collider.tag == "Obstacle")
{
active = true;
}
}
}
Second script (that read the boolean variable "active")
public class EmptyControllerColl : MonoBehaviour
{
public CollisionController controller;
public PlayerMovement movement;
public bool activeLocal = false;
private void Start()
{
GetComponentInChildren<CollisionController>();
}
void Update()
{
activeLocal = controller.active;
if(activeLocal == false)
{
Debug.Log("Nothing...");
}
if(activeLocal == true)
{
Debug.Log("Game over");
}
}
}
When the variable bool "Active" change its status, the variable "activeLocal" don't change status.. How can I resolve this problem?
Collision Controller is "connect" to Cube Object.
EmptyControllerColl is "connect" to emptyGameObject (parent of Cube).
This line
_ = GameObject.Find("cubo Variant").GetComponent<CollisionController>().active;
makes no sense. First of all there is no field or variable declared with the name _ so this shouldn't even compile at all. And secondly what do you need this for? Rather store the according reference once in the controller field and reuse it later.
Then for your usecase there is no need at all to store the value in a local variable ... this makes things only more complicated. Simply where you need it get the value from controller.active.
Also do not use == for tags. Rather check via CompareTag. The problem is that == silently fails if you have any typo or the tag doesn't exist at all. CompareTag rather throws an error that the given tag is not valid.
public class EmptyControllerColl : MonoBehaviour
{
// Best already drag this in via the Inspector in Unity
[SerializeField] private CollisionController controller;
public PlayerMovement movement;
// As fallback get it ONCE on runtime
private void Awake()
{
// since you say the cube is a child of this empty object you do not use
// Find at all but can simply use GetComponentInChildren
if(!controller) controller = GetComponentInChildren<CollisionController>(true);
}
void Update()
{
// No need to store this in a local field at all
if(!controller.active)
{
Debug.Log("Nothing...");
}
// use if else since both cases are exclusive and you don't even need to check the value twice
else
{
Debug.Log("Game over");
}
}
}
Event Driven - part A
In general you should avoid poll checks for a bool value in Update and rather come up with a more event driven solution! An example could look like:
public class CollisionController : MonoBehaviour
{
public PlayerMovement movement;
// Here everyone who wants can add listeners that get called as soon as
// we invoke this event. We will do it everytime the 'active' value is changed
public event Action<bool> OnActiveStateChanged;
// Backing field for 'active'
private bool _active;
// Property that reads and writes '_active'
// Everytime it is assigned it also invokes 'OnActiveStateChanged'
private bool active
{
get { return _active; }
set
{
_active = value;
OnActiveStateChanged?.Invoke(_active);
}
}
private void OnCollisionEnter(Collision collision)
{
if(collision.collider.CompareTag("Obstacle"))
{
active = true;
}
}
}
Now you would register a listener for this event like
public class EmptyControllerColl : MonoBehaviour
{
// Best already drag this in via the Inspector in Unity
[SerializeField] private CollisionController controller;
public PlayerMovement movement;
// As fallback get it ONCE on runtime
private void Awake()
{
// since you say the cube is a child of this empty object you do not use
// Find at all but can simply use GetComponentInChildren
if(!controller) controller = GetComponentInChildren<CollisionController>(true);
// register a callback. It is allowed an save to unregister first
// which makes sure this is only registered exactly once
controller.OnActiveStateChanged -= HandleControlerActiveStateChanged;
controller.OnActiveStateChanged += HandleControlerActiveStateChanged;
}
private void HandleGameOver()
{
Debug.Log("Game over");
}
private void HandleControlerActiveStateChanged(bool value)
{
if(!value)
{
Debug.Log("Nothing...");
}
else
{
Debug.Log("Game over");
}
}
private void OnDestroy()
{
// always clean up listeners
controller.OnActiveStateChanged -= HandleControlerActiveStateChanged;
}
}
This now is way more efficient since you don't all time run an Update method. Instead the HandleControlerActiveStateChanged is only called when the value of active is actually changed.
Event Driven - part B
And then actually in your case there is need to use a bool at all you could use a simple event Action instead and remove all the bools entirely:
public class CollisionController : MonoBehaviour
{
public PlayerMovement movement;
public event Action OnGameOver;
private void OnCollisionEnter(Collision collision)
{
if(collision.collider.CompareTag("Obstacle"))
{
OnGameOver?.Invoke();
}
}
}
Now you would register a listener for this event like
public class EmptyControllerColl : MonoBehaviour
{
[SerializeField] private CollisionController controller;
public PlayerMovement movement;
private void Awake()
{
if(!controller) controller = GetComponentInChildren<CollisionController>(true);
controller.OnGameOver -= HandleGameOver;
controller.OnGameOver += HandleGameOver;
}
private void HandleGameOver()
{
Debug.Log("Game over");
}
private void OnDestroy()
{
controller.OnGameOver -= HandleGameOver;
}
}
using UnityEngine;
public class CollisionController : MonoBehaviour
{
void Start()
{
// Calls the function ApplyDamage with a value of 5
// Every script attached to the game object
// that has an ApplyDamage function will be called.
gameObject.SendMessage("ApplyDamage", 5.0);
}
}
public class EmptyControllerColl : MonoBehaviour
{
public void ApplyDamage(float damage)
{
print(damage);
}
}
https://docs.unity3d.com/ScriptReference/GameObject.SendMessage.html

Unity/C# How to run Script continuously on User Interface Select Event Trigger

Complete beginner here, I'm looking to smoothly rotate my sphere continuously while the button it is a child of is selected in the UI.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class ButtonHandler : MonoBehaviour
{
private Vector3 rotationDirection = new Vector3(0, .25f, 0);
private float smoothTime = 1;
private float convertedTime = 200;
private float smooth;
public void SetText()
{
Transform target = transform.Find("Globe");
smooth = Time.deltaTime * smoothTime * convertedTime;
target.Rotate(rotationDirection * smooth);
}
}
The issue is that when I go to select my button, the function is only ran a single time, whereas I want it to run continuously until deselected.
** Note this is using the Select Event Trigger
Any help would be greatly appreciated!
Please let me know if you need additional information to answer
Thanks!
The way I see it, you could use the Update() function (native to MonoBehaviour) to run the few lines of code that you have and rotate your sphere.
Then set up functions for when the button is clicked and when it is released to change a boolean variable that is evaluated in the Update() function...
Breif example below...
private bool rotate = false;
void Update()
{
if (rotate)
{
Transform target = transform.Find("Globe");
smooth = Time.deltaTime * smoothTime * convertedTime;
target.Rotate(rotationDirection * smooth);
}
}
public void StartRotation() //on click
{
rotate = true;
}
public void StopRotation() //on release
{
rotate = false;
}
Further improvements might include Finding the "Globe" object in the constructor and storing a reference to it for future use during rotation.
Another method involves setting the rotation speed of the sphere rather than directly transforming (rotating) it constantly (make the Physics Enginge do the work) but try this first to get it working.
Extending Nicholas Sims' answer a slightly better option would be a Coroutine. Coroutines can be seen as temporary Update method as by default each iteration step is Don right after the Update call.
This way you don't waste resources to an Update method when the trigger is not activated anyway.
Also note that Find in general is a very expensive call and should be avoided wherever possible! The least you want to do is using it in Update every frame! Instead store the reference as early as possible .. maybe even already via a field in the Inspector!
// For storing the reference
// If possible already reference this via the Inspector
// by drag&drop to avoid Find entirely!
[SerializeField] private Transform target;
private bool rotate;
private void Start()
{
if(!target) target = GameObject.Find("Globe");
}
public void ToggleRoutine()
{
rotate = !rotate;
if(!rotate)
{
StopAllCoroutines();
}
else
{
StartCoroutine (RotateContinuously());
}
}
private IEnumerator RotateContinuously()
{
// Whut? o.O
// No worries, this is fine in a Coroutine as long as you yield somewhere inside
while (true)
{
// You should use Find only once .. if at all. You should avoid it wherever possible!
if(!target) target = transform.Find("Globe");
smooth = Time.deltaTime * smoothTime * convertedTime;
target.Rotate(rotationDirection * smooth);
// Very important for not freezing the editor completely!
// This tells unity to "pause" the routine here
// render the current frame and continue
// from this point on the next frame
yield return null;
}
}
If you rather want a "While Pressed" behaviour, so you want the rotation going on while the user holds the mouse/button pressed and stop as soon as he released it, you can use the IPointerXYHandler interfaces like
public class WhilePressedRotation : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler
{
[SerializeField] private Transform target;
private void Start ()
{
if(!target) target = GameObject.Find("Globe");
}
public void OnPointerDown(PointerEventData pointerEventData)
{
StartCoroutine(RotateContinuously());
}
public void OnPointerUp(PointerEventData pointerEventData)
{
StopAllCoroutines();
}
public void OnPointerEnter(PointerEventData pointerEventData)
{
// Actually not really needed
// but not sure if maybe required for having OnPointerExit work
}
public void OnPointerExit(PointerEventData pointerEventData)
{
StopAllCoroutines();
}
private IEnumerator RotateContinuously()
{
// Whut? o.O
// No worries, this is fine in a Coroutine as long as you yield somewhere inside
while (true)
{
// You should use Find only once .. if at all. You should avoid it wherever possible!
if(!target) target = transform.Find("Globe");
smooth = Time.deltaTime * smoothTime * convertedTime;
target.Rotate(rotationDirection * smooth);
// Very important for not freezing the editor completely!
// This tells unity to "pause" the routine here
// render the current frame and continue
// from this point on the next frame
yield return null;
}
}
}
Or a more general code
public class WhilePressedButton : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler
{
[SerializeField] private UnityEvent whilePressed;
public void OnPointerDown(PointerEventData pointerEventData)
{
StartCoroutine(ContinuousButton());
}
public void OnPointerUp(PointerEventData pointerEventData)
{
StopAllCoroutines();
}
public void OnPointerEnter(PointerEventData pointerEventData)
{
// Actually not really needed
// but not sure if maybe required for having OnPointerExit work
}
public void OnPointerExit(PointerEventData pointerEventData)
{
StopAllCoroutines();
}
private IEnumerator ContinuousButton()
{
// Whut? o.O
// No worries, this is fine in a Coroutine as long as you yield somewhere inside
while (true)
{
whilePressed.Invoke();
// Very important for not freezing the editor completely!
// This tells unity to "pause" the routine here
// render the current frame and continue
// from this point on the next frame
yield return null;
}
}
}
And reference a method in whilePressed like usually in a button onClick via Inspector or code.

Delegates and Events triggering on particular instance

I have the following class which defines an event and delegate. I then have a method which fires the event like this:
public class HealthManager : MonoBehaviour {
// Create the delegate
public delegate void ObjectDeath(GameObject go);
// Create the event
public static event ObjectDeath OnObjectDeath;
public static void GiveDamage(GameObject dealer, GameObject receiver, float amount){
/*
Subtract health here
... Code Snipped ...
*/
if(objectHealth.health <= objectHealth.deathHealth){
// Trigger the delegate/event
OnObjectDeath(receiver);
}
}
}
I then have this class which listens for the event to get fired like this:
public class DeathListener : MonoBehaviour {
// Add event listeners when object gets enabled
void OnEnable () {
HealthManager.OnObjectDeath += Died;
}
// Remove event listeners when object gets disabled
void OnDisable () {
HealthManager.OnObjectDeath -= Died;
}
void Died(GameObject go){
if(gameObject == go){
Destroy(gameObject);
}
}
}
The question I have is, is it possible to fire the event on just particular objects?
So, instead of doing this:
// Trigger
OnObjectDeath(receiver);
// ---------------------------- //
void Died(GameObject go){
if(gameObject == go){
Destroy(gameObject);
}
}
I can do something like this:
receiver.OnObjectDeath();
// ---------------------------- //
void Died(){
Destroy(gameObject);
}
I would like to have a way where in the event Died I don't have to check which object to apply it on. Currently if I remove the if statement, then it will apply that to all gameObjects not just the instance I am working with.
You seem to misuse the event pattern in your case. The point of event/listener is that the sender sends the info regardless of who is listening.
But in your case, the sender is aiming at one specific instance and you actually have a reference to it already. Then you might as well get the component of that object and call a method on it.
public static void GiveDamage(GameObject dealer, GameObject receiver, float amount){
/*
Subtract health here
... Code Snipped ...
*/
if(objectHealth.health <= objectHealth.deathHealth){
// Trigger the delegate/event
receiver.GetComponent< DeathListener >().Died(); // no need for parameter
}
}
EDIT: Based on your comment, a more appropriate way (in my opinion) would be to have a IDamageable interface with Damage method. Then all items that should be damaged should implement the method so they get damaged properly. Then you can simply calls the Damage method with the amount of damage and it will take care of the death if needed.
Let's consider a bullet system:
public void Bullet:MonoBehaviour{
public int damage = 10;
// it is hitting either from raycast or collision or else
void OnHit(GameObject collidedObject){
IDamageable dam = collidedObject.GetComponent<IDamageable>();
// if you hit a wall or no damageable item, it will be false
if(dam != null) {
dam.Damage(damage);
}
}
}
The bullet is oblivious to what it is hitting and should not care. Only matters how much it should damage.
public class Player : MonoBehaviour , IDamageable{
int health = 20;
public EventHandler <EventArgs> RaiseDeathEvent;
protected void OnDeath(EventArgs arg){
if(RaiseDeathEvent != null) RaiseDeathEvent(this, arg);
}
public void Damage(int damage){
this.health -= damage;
if(this.health < 0){
Died();
OnDeath(null);
}
}
}
There you go. If you need other items, like UI or scene manager to be informed of death so that they can perform an action, then you would get those to find the event and register to it. The player would not even know about them.
The problem is with the static event because it will be shared across the HealthManager class instances so i did this :
public class HealthManager : MonoBehaviour {
// Create the delegate
public delegate void ObjectDeath(GameObject go);
// Create the event
public event ObjectDeath OnObjectDeath;
//this method must not be marked static also
public void GiveDamage(GameObject dealer, GameObject receiver, float amount){
/*
Subtract health here
... Code Snipped ...
*/
if(objectHealth.health <= objectHealth.deathHealth){
// Trigger the delegate/event
OnObjectDeath();
}
}
}
And in the DeathListener class we have :
public class DeathListener : MonoBehaviour {
public HealthManager _hm;
void Awake(){
_hm = GetComponent<HealthManager>();
}
// Add event listeners when object gets enabled
void OnEnable () {
_hm.OnObjectDeath += Died;
}
// Remove event listeners when object gets disabled
void OnDisable () {
_hm.OnObjectDeath -= Died;
}
void Died(){
Destroy(gameObject);
} }
Now only the owner of each component will be destroyed separately not all of them together
Please consider any syntax error in the code because i wrote this from your own partial code ...
Further note : you can even add a [RequireComponent (typeof (HealthManager))] to your DeathListener class so it can't be added without the healthmanager !

Categories