Use EventSystem for key-pressing events - c#

Context: say you're checking whether "W" is pressed on the keyboard, the most common way to check this is through the following code:
void Update(){
if (Input.GetKeyDown("W"))
DoSomething();
}
Is there a way to do the following, using Unity's EventSystem? In other words, is there an implementation of an interface like IPointerClickHandler, for example, to check whether a button is pressed, without doing so in an Update() function?

Is there a way to do the following, using Unity's EventSystem? In other words, is there an implementation of an interface like IPointerClickHandler,
No. The EventSystem is mostly used for raycasting and dispatching events. This is not used to detect keyboard events. The only component from the EventSystem that can detect keyboard events is the InputField component. That's it and it can't be used for anything else.
Check whether a button is pressed, without doing so in an Update()
function?
Yes, there is a way with Event.KeyboardEvent and this requires the OnGUI function.
void OnGUI()
{
if (Event.current.Equals(Event.KeyboardEvent("W")))
{
print("W pressed!");
}
}
This is worse than using the Input.GetKeyDown function with the Update function. I encourage you to stick with Input.GetKeyDown. There is nothing wrong with it.
If you are looking for event type InputSystem without Input.GetKeyDown then use Unity's new Input API and subscribe to the InputSystem.onEvent event.
If you are looking for feature similar to the IPointerClickHandler interface you can implement it on top of Input.GetKeyDown.
1.First, get all the KeyCode enum with System.Enum.GetValues(typeof(KeyCode)); and store it in an array.
2.Create an interface "IKeyboardEvent" and add functions such as OnKeyDown just like OnPointerClick in the IPointerClickHandler interface.
3.Loop through the KeyCode from #1 and check if each key in the array is pressed, released or held down.
4.Get all the components in the scene and check if they implemented the IKeyboardEvent interface. If they do, invoke the proper function in the interface based on the key status from #3.
Here is a functional example that can still be extended or improved:
Attach to an empty GameObject.
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class KeyboardEventSystem : MonoBehaviour
{
Array allKeyCodes;
private static List<Transform> allTransforms = new List<Transform>();
private static List<GameObject> rootGameObjects = new List<GameObject>();
void Awake()
{
allKeyCodes = System.Enum.GetValues(typeof(KeyCode));
}
void Update()
{
//Loop over all the keycodes
foreach (KeyCode tempKey in allKeyCodes)
{
//Send event to key down
if (Input.GetKeyDown(tempKey))
senEvent(tempKey, KeybrdEventType.keyDown);
//Send event to key up
if (Input.GetKeyUp(tempKey))
senEvent(tempKey, KeybrdEventType.KeyUp);
//Send event to while key is held down
if (Input.GetKey(tempKey))
senEvent(tempKey, KeybrdEventType.down);
}
}
void senEvent(KeyCode keycode, KeybrdEventType evType)
{
GetAllRootObject();
GetAllComponents();
//Loop over all the interfaces and callthe appropriate function
for (int i = 0; i < allTransforms.Count; i++)
{
GameObject obj = allTransforms[i].gameObject;
//Invoke the appropriate interface function if not null
IKeyboardEvent itfc = obj.GetComponent<IKeyboardEvent>();
if (itfc != null)
{
if (evType == KeybrdEventType.keyDown)
itfc.OnKeyDown(keycode);
if (evType == KeybrdEventType.KeyUp)
itfc.OnKeyUP(keycode);
if (evType == KeybrdEventType.down)
itfc.OnKey(keycode);
}
}
}
private static void GetAllRootObject()
{
rootGameObjects.Clear();
Scene activeScene = SceneManager.GetActiveScene();
activeScene.GetRootGameObjects(rootGameObjects);
}
private static void GetAllComponents()
{
allTransforms.Clear();
for (int i = 0; i < rootGameObjects.Count; ++i)
{
GameObject obj = rootGameObjects[i];
//Get all child Transforms attached to this GameObject
obj.GetComponentsInChildren<Transform>(true, allTransforms);
}
}
}
public enum KeybrdEventType
{
keyDown,
KeyUp,
down
}
public interface IKeyboardEvent
{
void OnKeyDown(KeyCode keycode);
void OnKeyUP(KeyCode keycode);
void OnKey(KeyCode keycode);
}
Usage:
Implement the IKeyboardEvent interface and the functions from it in your script just like you would with IPointerClickHandler.
public class test : MonoBehaviour, IKeyboardEvent
{
public void OnKey(KeyCode keycode)
{
Debug.Log("Key held down: " + keycode);
}
public void OnKeyDown(KeyCode keycode)
{
Debug.Log("Key pressed: " + keycode);
}
public void OnKeyUP(KeyCode keycode)
{
Debug.Log("Key released: " + keycode);
}
}

I created an simples script to trigger simple event.
I used OnguiGUI instead of Update.
See it bellow!
using UnityEngine;
using UnityEngine.Events;
public class TriggerKey : MonoBehaviour
{
[Header("----Add key to trigger on pressed----")]
public string key;
// Unity event inspector
public UnityEvent OnTriggerKey;
public void OnGUI()
{ // triiger event on trigger key
if (Event.current.Equals(Event.KeyboardEvent(key)))
{
OnTriggerKey.Invoke();
print("test trigger btn");
}
}
}

Related

Event to detect ActiveSelf

Is there a listener event to detect if an object is set to inactive from active state or to active from inactive state? I do not want to add it in Update as there will be multiple calls and it would affect my game's performance. So is there an alternative for this?
public GameObject Go_1;
public GameObject Go_2;
void Update () {
if (Go_1.activeSelf) {
} else if (Go_2.activeSelf) {
}
}
You could implement something using the Update method like e.g.
public class ActiveSelfWatcher : MonoBehaviour
{
private Dictionary<ActiveSelfProvider, bool> _lastActiveSelfState = new Dictionary<ActiveSelfProvider, bool>();
private void OnObjectBecameActive(GameObject obj)
{
Debug.Log($"{obj.name} became active!", this);
}
private void OnObjectBecameInactive(GameObject obj)
{
Debug.Log($"{obj.name} became inactive!", this);
}
private void Update()
{
// Iterate through all registered instances of ActiveSelfProvider
foreach(var provider in ActiveSelfProvider.Instances)
{
// pre-cache the GameObject reference
var obj = provider.gameObject;
// pre-cache the current activeSelf state
var currentActive = obj.activeSelf;
if(!_lastActiveSelfState.TryGetValue(provider))
{
// we don't know this provider until now
// TODO here you have to decide whether you want to call the events now once for this provider or not
// TODO otherwise it is only called for providers you already know and changed their state
}
if(currentActive != _lastActiveSelfState[provider])
{
// the state is not the one we stored for this instance
// => it changed its states since the last frame
// Call the according "event"
if(currentActive)
{
OnObjectBecameActive(obj);
}
else
{
OnObjectBecameInactive(obj);
}
}
// store the current value
_lastActiveSelfState[provider] = currentActive;
}
}
}
This is your watcher class you currently already have anyway.
Then on all the objects you want to be able to watch you use
public class ActiveSelfProvider : MonoBehaviour
{
private static readonly HashSet<ActiveSelfProvider> instances = new HashSet<ActiveSelfProvider>();
public static HashSet<ActiveSelfProvider> Instances => new HashSet<ActiveSelfProvider>(instances);
private void Awake()
{
// register your self to the existing instances
instances.Add(this);
}
private void OnDestroy()
{
// remove yourself from the existing instances
instances.Remove(this);
}
}
If this is "efficient enough" for you use case you would have to test ;)
If you want to go super fancy, someone once made a Transform Interceptor .. a quite nasty hack which on compile time overrules parts of the Unity built-in Transform property setters to hook in callbacks.
One probably could create something like this also for SetActive ;)
Note: Typed on smartphone but I hope the idea gets clear
Yes, try with OnEnable and OnDisable.
// Implement OnDisable and OnEnable script functions.
// These functions will be called when the attached GameObject
// is toggled.
// This example also supports the Editor. The Update function
// will be called, for example, when the position of the
// GameObject is changed.
using UnityEngine;
[ExecuteInEditMode]
public class PrintOnOff : MonoBehaviour
{
void OnDisable()
{
Debug.Log("PrintOnDisable: script was disabled");
}
void OnEnable()
{
Debug.Log("PrintOnEnable: script was enabled");
}
void Update()
{
#if UNITY_EDITOR
Debug.Log("Editor causes this Update");
#endif
}
}

Unity3D: How to do object pooling without a spawner singleton

Usually, if you use object pooling, you make a singleton like in this video.
After seeing this video, I discovered how messy singleton can be. Is there any other way to do object pooling without using singletons? I wanna instead use Events.
You would need to hold the pool in a class which is not a singleton, and handle your gameobject pool according to your events.
Regarding to call them with events, "I want to use events" is not a very concrete question. You need to set your events to listen (method subscribe) and to call them in the code wherever they're supposed to occur, this is invoke the method. I suggest that if you are not clear about this, try to use the unity events (OnTriggerEnter, if(Input.GetMouseButtonDown(0)) in the Update etc) until you dig in the topic enough to understand them and make ones of you own with c# events or UnityEvents when needed.
Find two template scripts, a pool and and event handler to handle your objects in the scene. You can check those out in an empty scene with your respective two gameObject to attach, and the object you want in the pool, pressing 'space' and 'A' to create from pool and return to pool respectively.
Pool manager:
using System.Collections.Generic;
using UnityEngine;
public class PoolManager : MonoBehaviour
{
private Queue<GameObject> objPool;
private Queue<GameObject> activeObj;
private int poolSize = 10;
public GameObject objPrefab;
void Start()
{
//queues init
objPool = new Queue<GameObject>();
activeObj = new Queue<GameObject>();
//pool init
for (int i = 0; i < poolSize; i++)
{
GameObject newObj = Instantiate(objPrefab);
objPool.Enqueue(newObj);
newObj.SetActive(false);
}
}
public GameObject GetRandomActiveGO() {
GameObject lastActive = default;
if (activeObj.Count > 0)
lastActive = activeObj.Dequeue();
else {
Debug.LogError("Active object queue is empty");
}
return lastActive;
}
//get from pool
public GameObject GetObjFromPool(Vector3 newPosition, Quaternion newRotation)
{
GameObject newObject = objPool.Dequeue();
newObject.SetActive(true);
newObject.transform.SetPositionAndRotation(newPosition, newRotation);
//keep actives to be retrieved
activeObj.Enqueue(newObject);
return newObject;
}
//return to pool
public void ReturnObjToPool(GameObject go)
{
go.SetActive(false);
objPool.Enqueue(go);
}
}
Event handler:
using UnityEngine;
public class EventHandler : MonoBehaviour
{
public delegate GameObject OnSpacePressed(Vector3 newPosition, Quaternion newRotation);
public OnSpacePressed onSpacePressed;
public delegate void OnAKeyPressed(GameObject go);
public OnAKeyPressed onAKeyPressed;
public PoolManager poolManager;
void Start()
{
onSpacePressed = poolManager.GetObjFromPool;
onAKeyPressed = poolManager.ReturnObjToPool;
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
onSpacePressed?.Invoke(new Vector3(0, 0, 0), Quaternion.identity);
}
//here I get a random active, however this would be called in the specific objects remove circumstances,
//so you should have a reference to that specific gameobje when rerunrning it to the pool.
if (Input.GetKeyDown(KeyCode.A))
{
GameObject go = poolManager.GetRandomActiveGO();
onAKeyPressed?.Invoke(go);
}
}
}
Edit: Singleton pattern
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
protected static T _instance;
public static T instance
{
get
{
if (_instance == null)
{
_instance = GameObject.FindObjectOfType<T>();
if (_instance == null)
{
_instance = new GameObject(typeof(T).Name).AddComponent<T>();
}
}
return _instance;
}
}
}

Unity can't identify which button is beeing pressed

I've been trying to make the options in my game being selected by a keyboard input. I can highlight them, but I don't know how to make Unity identify which of the buttons is beeing pressed in order to do a certain action, it's giving me the NullReferenceException in line 28 of my code. The cript in question is the BattleSystem script, it's attached to the event system, the battleFirstButton is the fight button and the enterKey is "Z".
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public class BattleSystem : MonoBehaviour
{
public GameObject battleFirstButton;
public KeyCode enterKey;
Button selectedButton;
// Start is called before the first frame update
void Start()
{
EventSystem.current.SetSelectedGameObject(null);
EventSystem.current.SetSelectedGameObject(battleFirstButton);
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(enterKey))
{
selectedButton.onClick.Invoke();
}
}
public void SetSelectedButton()
{
selectedButton = GetComponent<Button>();
}
public void Fight()
{
print("Fight option submitted");
}
public void Act()
{
print("Act option submitted");
}
public void Item()
{
print("Item option submitted");
}
public void Mercy()
{
print("Get dunked o-, I mean, Mercy option selected");
}
}
selectedButton is a private variable potentially never set to anything, so it's null. Make sure it's set to something before you access it.
Probably the simplest fix to the way you have it set up is:
void Update()
{
if (Input.GetKeyDown(enterKey))
{
// Gets the focused button
selectedButton = EventSystem.current.currentSelectedGameObject.GetComponent<Button>();
if (selectedButton != null)
{
selectedButton.onClick.Invoke();
}
}

Using Unity3D's IPointerDownHandler approach, but with "the whole screen"

In Unity say you need to detect finger touch (finger drawing) on something in the scene.
The only way to do this in modern Unity, is very simple:
Step 1. Put a collider on that object. ("The ground" or whatever it may be.) 1
Step 2. On your camera, Inspector panel, click to add a Physics Raycaster (2D or 3D as relevant).
Step 3. Simply use code as in Example A below.
(Tip - don't forget to ensure there's an EventSystem ... sometimes Unity adds one automatically, sometimes not!)
Fantastic, couldn't be easier. Unity finally handles un/propagation correctly through the UI layer. Works uniformly and flawlessly on desktop, devices, Editor, etc etc. Hooray Unity.
All good. But what if you want to draw just "on the screen"?
So you are wanting, quite simply, swipes/touches/drawing from the user "on the screen". (Example, simply for operating an orbit camera, say.) So just as in any ordinary 3D game where the camera runs around and moves.
You don't want the position of the finger on some object in world space, you simply want abstract "finger motions" (i.e. position on the glass).
What collider do you then use? Can you do it with no collider? It seems fatuous to add a collider just for that reason.
What we do is this:
I just make a flat collider of some sort, and actually attach it under the camera. So it simply sits in the camera frustum and completely covers the screen.
(For the code, there is then no need to use ScreenToWorldPoint, so just use code as in Example B - extremely simple, works perfectly.)
My question, it seems a bit odd to have to use the "under-camera colldier" I describe, just to get touches on the glass.
What's the deal here?
(Note - please don't answer involving Unity's ancient "Touches" system, which is unusable today for real projects, you can't ignore .UI using the legacy approach.)
Code sample A - drawing on a scene object. Use ScreenToWorldPoint.
using UnityEngine;
using System.Collections;
using UnityEngine.EventSystems;
public class FingerMove:MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler
{
public void OnPointerDown (PointerEventData data)
{
Debug.Log("FINGER DOWN");
prevPointWorldSpace =
theCam.ScreenToWorldPoint( data.position );
}
public void OnDrag (PointerEventData data)
{
thisPointWorldSpace =
theCam.ScreenToWorldPoint( data.position );
realWorldTravel =
thisPointWorldSpace - prevPointWorldSpace;
_processRealWorldtravel();
prevPointWorldSpace = thisPointWorldSpace;
}
public void OnPointerUp (PointerEventData data)
{
Debug.Log("clear finger...");
}
Code sample B ... you only care about what the user does on the glass screen of the device. Even easier here:
using UnityEngine;
using System.Collections;
using UnityEngine.EventSystems;
public class FingerMove:MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler
{
private Vector2 prevPoint;
private Vector2 newPoint;
private Vector2 screenTravel;
public void OnPointerDown (PointerEventData data)
{
Debug.Log("FINGER DOWN");
prevPoint = data.position;
}
public void OnDrag (PointerEventData data)
{
newPoint = data.position;
screenTravel = newPoint - prevPoint;
prevPoint = newPoint;
_processSwipe();
}
public void OnPointerUp (PointerEventData data)
{
Debug.Log("FINEGR UP...");
}
private void _processSwipe()
{
// your code here
Debug.Log("screenTravel left-right.. " + screenTravel.x.ToString("f2"));
}
}
1 If you're just new to Unity: at that step very likely, make it a layer called say "Draw"; in physics settings make "Draw" interact with nothing; in step two with the Raycaster just set the layer to "Draw".
First of all, you need to understand that there are just 3 ways to detect click on an Object with the OnPointerDown function:
1.You need a UI component to in order to detect click with the OnPointerDown function. This applies to other similar UI events.
2.Another method to detect a click with the OnPointerDown function on a 2D/Sprite GameObject is to attach Physics2DRaycaster to the Camera and then OnPointerDown will be called when it is clicked. Note that a 2D Collider must be attached to it.
3.If this is a 3D Object with a Collider not 2D Collider, you must have PhysicsRaycaster attached to the camera in order for the OnPointerDown function to be called.
Doing this with the first method seems more reasonable instead of having a large collider or 2D collider covering the screen. All you do is to create a Canvas, Panel GameObject, and attach Image component that stretches across the whole screen to it.
Dude I just don't see using .UI as a serious solution: imagine we're
doing a big game and you're leading a team that is doing all the UI (I
mean buttons, menus and all). I'm leading a team doing the walking
robots. I suddenly say to you "oh, by the way, I can't handle touch
("!"), could you drop in a UI.Panel, don't forget to keep it under
everything you're doing, oh and put one on any/all canvasses or
cameras you swap between - and pass that info back to me OK!" :) I
mean it's just silly. One can't essentially say: "oh, Unity doesn't
handle touch"
Not really hard like the way you described it. You can write a long code that will be able to create a Canvas, Panel and an Image. Change the image alpha to 0. All you have to do is attach that code to the Camera or an empty GameObject and it will perform all this for you automatically on play mode.
Make every GameObject that wants to receive event on the screen subscribe to it, then use ExecuteEvents.Execute to send the event to all the interfaces in the script attached to that GameObject.
For example, the sample code below will send OnPointerDown event to the GameObject called target.
ExecuteEvents.Execute<IPointerDownHandler>(target,
eventData,
ExecuteEvents.pointerDownHandler);
Problem you will run into:
The hidden Image component will block other UI or GameObject from receiving raycast. This is the biggest problem here.
Solution:
Since it will cause some blocking problems, it is better to make the Canvas of the Image to be on top of everything. This will make sure that it is now 100 blocking all other UI/GameObject. Canvas.sortingOrder = 12; should help us do this.
Each time we detect an event such as OnPointerDown from the Image, we will manually send resend the OnPointerDown event to all other UI/GameObjects beneath the Image.
First of all, we throw a raycast with GraphicRaycaster(UI), Physics2DRaycaster(2D collider), PhysicsRaycaster(3D Collider) and store the result in a List.
Now, we loop over the result in the List and resend the event we received by sending artificial event to the results with:
ExecuteEvents.Execute<IPointerDownHandler>(currentListLoop,
eventData,
ExecuteEvents.pointerDownHandler);
Other problems you will run into:
You won't be able to send emulate events on the Toggle component with GraphicRaycaster. This is a bug in Unity. It took me 2 days to realize this.
Also was not able to send fake slider move event to the Slider component. I can't tell if this is a bug or not.
Other than these problems mentioned above, I was able to implement this. It comes in 3 parts. Just create a folder and put all the scripts in them.
SCRIPTS:
1.WholeScreenPointer.cs - The main part of the script that creates Canvas, GameObject, and hidden Image. It does all the complicated stuff to make sure that the Image always covers the screen. It also sends event to all the subscribe GameObject.
public class WholeScreenPointer : MonoBehaviour
{
//////////////////////////////// SINGLETON BEGIN ////////////////////////////////
private static WholeScreenPointer localInstance;
public static WholeScreenPointer Instance { get { return localInstance; } }
public EventUnBlocker eventRouter;
private void Awake()
{
if (localInstance != null && localInstance != this)
{
Destroy(this.gameObject);
}
else
{
localInstance = this;
}
}
//////////////////////////////// SINGLETON END ////////////////////////////////
//////////////////////////////// SETTINGS BEGIN ////////////////////////////////
public bool simulateUIEvent = true;
public bool simulateColliderEvent = true;
public bool simulateCollider2DEvent = true;
public bool hideWholeScreenInTheEditor = false;
//////////////////////////////// SETTINGS END ////////////////////////////////
private GameObject hiddenCanvas;
private List<GameObject> registeredGameobjects = new List<GameObject>();
//////////////////////////////// USEFUL FUNCTIONS BEGIN ////////////////////////////////
public void registerGameObject(GameObject objToRegister)
{
if (!isRegistered(objToRegister))
{
registeredGameobjects.Add(objToRegister);
}
}
public void unRegisterGameObject(GameObject objToRegister)
{
if (isRegistered(objToRegister))
{
registeredGameobjects.Remove(objToRegister);
}
}
public bool isRegistered(GameObject objToRegister)
{
return registeredGameobjects.Contains(objToRegister);
}
public void enablewholeScreenPointer(bool enable)
{
hiddenCanvas.SetActive(enable);
}
//////////////////////////////// USEFUL FUNCTIONS END ////////////////////////////////
// Use this for initialization
private void Start()
{
makeAndConfigWholeScreenPinter(hideWholeScreenInTheEditor);
}
private void makeAndConfigWholeScreenPinter(bool hide = true)
{
//Create and Add Canvas Component
createCanvas(hide);
//Add Rect Transform Component
//addRectTransform();
//Add Canvas Scaler Component
addCanvasScaler();
//Add Graphics Raycaster Component
addGraphicsRaycaster();
//Create Hidden Panel GameObject
GameObject panel = createHiddenPanel(hide);
//Make the Image to be positioned in the middle of the screen then fix its anchor
stretchImageAndConfigAnchor(panel);
//Add EventForwarder script
addEventForwarder(panel);
//Add EventUnBlocker
addEventRouter(panel);
//Add EventSystem and Input Module
addEventSystemAndInputModule();
}
//Creates Hidden GameObject and attaches Canvas component to it
private void createCanvas(bool hide)
{
//Create Canvas GameObject
hiddenCanvas = new GameObject("___HiddenCanvas");
if (hide)
{
hiddenCanvas.hideFlags = HideFlags.HideAndDontSave;
}
//Create and Add Canvas Component
Canvas cnvs = hiddenCanvas.AddComponent<Canvas>();
cnvs.renderMode = RenderMode.ScreenSpaceOverlay;
cnvs.pixelPerfect = false;
//Set Cavas sorting order to be above other Canvas sorting order
cnvs.sortingOrder = 12;
cnvs.targetDisplay = 0;
//Make it child of the GameObject this script is attached to
hiddenCanvas.transform.SetParent(gameObject.transform);
}
private void addRectTransform()
{
RectTransform rctrfm = hiddenCanvas.AddComponent<RectTransform>();
}
//Adds CanvasScaler component to the Canvas GameObject
private void addCanvasScaler()
{
CanvasScaler cvsl = hiddenCanvas.AddComponent<CanvasScaler>();
cvsl.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
cvsl.referenceResolution = new Vector2(800f, 600f);
cvsl.matchWidthOrHeight = 0.5f;
cvsl.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
cvsl.referencePixelsPerUnit = 100f;
}
//Adds GraphicRaycaster component to the Canvas GameObject
private void addGraphicsRaycaster()
{
GraphicRaycaster grcter = hiddenCanvas.AddComponent<GraphicRaycaster>();
grcter.ignoreReversedGraphics = true;
grcter.blockingObjects = GraphicRaycaster.BlockingObjects.None;
}
//Creates Hidden Panel and attaches Image component to it
private GameObject createHiddenPanel(bool hide)
{
//Create Hidden Panel GameObject
GameObject hiddenPanel = new GameObject("___HiddenPanel");
if (hide)
{
hiddenPanel.hideFlags = HideFlags.HideAndDontSave;
}
//Add Image Component to the hidden panel
Image pnlImg = hiddenPanel.AddComponent<Image>();
pnlImg.sprite = null;
pnlImg.color = new Color(1, 1, 1, 0); //Invisible
pnlImg.material = null;
pnlImg.raycastTarget = true;
//Make it child of HiddenCanvas GameObject
hiddenPanel.transform.SetParent(hiddenCanvas.transform);
return hiddenPanel;
}
//Set Canvas width and height,to matach screen width and height then set anchor points to the corner of canvas.
private void stretchImageAndConfigAnchor(GameObject panel)
{
Image pnlImg = panel.GetComponent<Image>();
//Reset postion to middle of the screen
pnlImg.rectTransform.anchoredPosition3D = new Vector3(0, 0, 0);
//Stretch the Image so that the whole screen is totally covered
pnlImg.rectTransform.anchorMin = new Vector2(0, 0);
pnlImg.rectTransform.anchorMax = new Vector2(1, 1);
pnlImg.rectTransform.pivot = new Vector2(0.5f, 0.5f);
}
//Adds EventForwarder script to the Hidden Panel GameObject
private void addEventForwarder(GameObject panel)
{
EventForwarder evnfwdr = panel.AddComponent<EventForwarder>();
}
//Adds EventUnBlocker script to the Hidden Panel GameObject
private void addEventRouter(GameObject panel)
{
EventUnBlocker evtrtr = panel.AddComponent<EventUnBlocker>();
eventRouter = evtrtr;
}
//Add EventSystem
private void addEventSystemAndInputModule()
{
//Check if EventSystem exist. If it does not create and add it
EventSystem eventSys = FindObjectOfType<EventSystem>();
if (eventSys == null)
{
GameObject evObj = new GameObject("EventSystem");
EventSystem evs = evObj.AddComponent<EventSystem>();
evs.firstSelectedGameObject = null;
evs.sendNavigationEvents = true;
evs.pixelDragThreshold = 5;
eventSys = evs;
}
//Check if StandaloneInputModule exist. If it does not create and add it
StandaloneInputModule sdlIpModl = FindObjectOfType<StandaloneInputModule>();
if (sdlIpModl == null)
{
sdlIpModl = eventSys.gameObject.AddComponent<StandaloneInputModule>();
sdlIpModl.horizontalAxis = "Horizontal";
sdlIpModl.verticalAxis = "Vertical";
sdlIpModl.submitButton = "Submit";
sdlIpModl.cancelButton = "Cancel";
sdlIpModl.inputActionsPerSecond = 10f;
sdlIpModl.repeatDelay = 0.5f;
sdlIpModl.forceModuleActive = false;
}
}
/*
Forwards Handler Event to every GameObject that implements IDragHandler, IPointerDownHandler, IPointerUpHandler interface
*/
public void forwardDragEvent(PointerEventData eventData)
{
//Route and send the event to UI and Colliders
for (int i = 0; i < registeredGameobjects.Count; i++)
{
ExecuteEvents.Execute<IDragHandler>(registeredGameobjects[i],
eventData,
ExecuteEvents.dragHandler);
}
//Route and send the event to UI and Colliders
eventRouter.routeDragEvent(eventData);
}
public void forwardPointerDownEvent(PointerEventData eventData)
{
//Send the event to all subscribed scripts
for (int i = 0; i < registeredGameobjects.Count; i++)
{
ExecuteEvents.Execute<IPointerDownHandler>(registeredGameobjects[i],
eventData,
ExecuteEvents.pointerDownHandler);
}
//Route and send the event to UI and Colliders
eventRouter.routePointerDownEvent(eventData);
}
public void forwardPointerUpEvent(PointerEventData eventData)
{
//Send the event to all subscribed scripts
for (int i = 0; i < registeredGameobjects.Count; i++)
{
ExecuteEvents.Execute<IPointerUpHandler>(registeredGameobjects[i],
eventData,
ExecuteEvents.pointerUpHandler);
}
//Route and send the event to UI and Colliders
eventRouter.routePointerUpEvent(eventData);
}
}
2.EventForwarder.cs - It simply receives any event from the hidden Image and passes it to the WholeScreenPointer.cs script for processing.
public class EventForwarder : MonoBehaviour, IDragHandler, IPointerDownHandler, IPointerUpHandler
{
WholeScreenPointer wcp = null;
void Start()
{
wcp = WholeScreenPointer.Instance;
}
public void OnDrag(PointerEventData eventData)
{
wcp.forwardDragEvent(eventData);
}
public void OnPointerDown(PointerEventData eventData)
{
wcp.forwardPointerDownEvent(eventData);
}
public void OnPointerUp(PointerEventData eventData)
{
wcp.forwardPointerUpEvent(eventData);
}
}
3.EventUnBlocker.cs - It unblocks the the rays the hidden Image is blocking by sending fake event to any Object above it. Be it UI, 2D or 3D collider.
public class EventUnBlocker : MonoBehaviour
{
List<GraphicRaycaster> grRayCast = new List<GraphicRaycaster>(); //UI
List<Physics2DRaycaster> phy2dRayCast = new List<Physics2DRaycaster>(); //Collider 2D (Sprite Renderer)
List<PhysicsRaycaster> phyRayCast = new List<PhysicsRaycaster>(); //Normal Collider(3D/Mesh Renderer)
List<RaycastResult> resultList = new List<RaycastResult>();
//For Detecting button click and sending fake Button Click to UI Buttons
Dictionary<int, GameObject> pointerIdToGameObject = new Dictionary<int, GameObject>();
// Use this for initialization
void Start()
{
}
public void sendArtificialUIEvent(Component grRayCast, PointerEventData eventData, PointerEventType evType)
{
//Route to all Object in the RaycastResult
for (int i = 0; i < resultList.Count; i++)
{
/*Do something if it is NOT this GameObject.
We don't want any other detection on this GameObject
*/
if (resultList[i].gameObject != this.gameObject)
{
//Check if this is UI
if (grRayCast is GraphicRaycaster)
{
//Debug.Log("UI");
routeEvent(resultList[i], eventData, evType, true);
}
//Check if this is Collider 2D/SpriteRenderer
if (grRayCast is Physics2DRaycaster)
{
//Debug.Log("Collider 2D/SpriteRenderer");
routeEvent(resultList[i], eventData, evType, false);
}
//Check if this is Collider/MeshRender
if (grRayCast is PhysicsRaycaster)
{
//Debug.Log("Collider 3D/Mesh");
routeEvent(resultList[i], eventData, evType, false);
}
}
}
}
//Creates fake PointerEventData that will be used to make PointerEventData for the callback functions
PointerEventData createEventData(RaycastResult rayResult)
{
PointerEventData fakeEventData = new PointerEventData(EventSystem.current);
fakeEventData.pointerCurrentRaycast = rayResult;
return fakeEventData;
}
private void routeEvent(RaycastResult rayResult, PointerEventData eventData, PointerEventType evType, bool isUI = false)
{
bool foundKeyAndValue = false;
GameObject target = rayResult.gameObject;
//Make fake GameObject target
PointerEventData fakeEventData = createEventData(rayResult);
switch (evType)
{
case PointerEventType.Drag:
//Send/Simulate Fake OnDrag event
ExecuteEvents.Execute<IDragHandler>(target, fakeEventData,
ExecuteEvents.dragHandler);
break;
case PointerEventType.Down:
//Send/Simulate Fake OnPointerDown event
ExecuteEvents.Execute<IPointerDownHandler>(target,
fakeEventData,
ExecuteEvents.pointerDownHandler);
//Code Below is for UI. break out of case if this is not UI
if (!isUI)
{
break;
}
//Prepare Button Click. Should be sent in the if PointerEventType.Up statement
Button buttonFound = target.GetComponent<Button>();
//If pointerId is not in the dictionary add it
if (buttonFound != null)
{
if (!dictContains(eventData.pointerId))
{
dictAdd(eventData.pointerId, target);
}
}
//Bug in Unity with GraphicRaycaster and Toggle. Have to use a hack below
//Toggle Toggle component
Toggle toggle = null;
if ((target.name == "Checkmark" || target.name == "Label") && toggle == null)
{
toggle = target.GetComponentInParent<Toggle>();
}
if (toggle != null)
{
//Debug.LogWarning("Toggled!: " + target.name);
toggle.isOn = !toggle.isOn;
//Destroy(toggle.gameObject);
}
break;
case PointerEventType.Up:
//Send/Simulate Fake OnPointerUp event
ExecuteEvents.Execute<IPointerUpHandler>(target,
fakeEventData,
ExecuteEvents.pointerUpHandler);
//Code Below is for UI. break out of case if this is not UI
if (!isUI)
{
break;
}
//Send Fake Button Click if requirement is met
Button buttonPress = target.GetComponent<Button>();
/*If pointerId is in the dictionary, check
*/
if (buttonPress != null)
{
if (dictContains(eventData.pointerId))
{
//Check if GameObject matches too. If so then this is a valid Click
for (int i = 0; i < resultList.Count; i++)
{
GameObject tempButton = resultList[i].gameObject;
if (tempButton != this.gameObject && dictContains(eventData.pointerId, tempButton))
{
foundKeyAndValue = true;
//Debug.Log("Button ID and GameObject Match! Sending Click Event");
//Send/Simulate Fake Click event to the Button
ExecuteEvents.Execute<IPointerClickHandler>(tempButton,
new PointerEventData(EventSystem.current),
ExecuteEvents.pointerClickHandler);
}
}
}
}
break;
}
//Remove pointerId since it exist
if (foundKeyAndValue)
{
dictRemove(eventData.pointerId);
}
}
void routeOption(PointerEventData eventData, PointerEventType evType)
{
UpdateRaycaster();
if (WholeScreenPointer.Instance.simulateUIEvent)
{
//Loop Through All GraphicRaycaster(UI) and throw Raycast to each one
for (int i = 0; i < grRayCast.Count; i++)
{
//Throw Raycast to all UI elements in the position(eventData)
grRayCast[i].Raycast(eventData, resultList);
sendArtificialUIEvent(grRayCast[i], eventData, evType);
}
//Reset Result
resultList.Clear();
}
if (WholeScreenPointer.Instance.simulateCollider2DEvent)
{
//Loop Through All Collider 2D (Sprite Renderer) and throw Raycast to each one
for (int i = 0; i < phy2dRayCast.Count; i++)
{
//Throw Raycast to all UI elements in the position(eventData)
phy2dRayCast[i].Raycast(eventData, resultList);
sendArtificialUIEvent(phy2dRayCast[i], eventData, evType);
}
//Reset Result
resultList.Clear();
}
if (WholeScreenPointer.Instance.simulateColliderEvent)
{
//Loop Through All Normal Collider(3D/Mesh Renderer) and throw Raycast to each one
for (int i = 0; i < phyRayCast.Count; i++)
{
//Throw Raycast to all UI elements in the position(eventData)
phyRayCast[i].Raycast(eventData, resultList);
sendArtificialUIEvent(phyRayCast[i], eventData, evType);
}
//Reset Result
resultList.Clear();
}
}
public void routeDragEvent(PointerEventData eventData)
{
routeOption(eventData, PointerEventType.Drag);
}
public void routePointerDownEvent(PointerEventData eventData)
{
routeOption(eventData, PointerEventType.Down);
}
public void routePointerUpEvent(PointerEventData eventData)
{
routeOption(eventData, PointerEventType.Up);
}
public void UpdateRaycaster()
{
convertToList(FindObjectsOfType<GraphicRaycaster>(), grRayCast);
convertToList(FindObjectsOfType<Physics2DRaycaster>(), phy2dRayCast);
convertToList(FindObjectsOfType<PhysicsRaycaster>(), phyRayCast);
}
//To avoid ToList() function
void convertToList(GraphicRaycaster[] fromComponent, List<GraphicRaycaster> toComponent)
{
//Clear and copy new Data
toComponent.Clear();
for (int i = 0; i < fromComponent.Length; i++)
{
toComponent.Add(fromComponent[i]);
}
}
//To avoid ToList() function
void convertToList(Physics2DRaycaster[] fromComponent, List<Physics2DRaycaster> toComponent)
{
//Clear and copy new Data
toComponent.Clear();
for (int i = 0; i < fromComponent.Length; i++)
{
toComponent.Add(fromComponent[i]);
}
}
//To avoid ToList() function
void convertToList(PhysicsRaycaster[] fromComponent, List<PhysicsRaycaster> toComponent)
{
//Clear and copy new Data
toComponent.Clear();
for (int i = 0; i < fromComponent.Length; i++)
{
toComponent.Add(fromComponent[i]);
}
}
//Checks if object is in the dictionary
private bool dictContains(GameObject obj)
{
return pointerIdToGameObject.ContainsValue(obj);
}
//Checks if int is in the dictionary
private bool dictContains(int pointerId)
{
return pointerIdToGameObject.ContainsKey(pointerId);
}
//Checks if int and object is in the dictionary
private bool dictContains(int pointerId, GameObject obj)
{
return (pointerIdToGameObject.ContainsKey(pointerId) && pointerIdToGameObject.ContainsValue(obj));
}
//Adds pointerId and its value to dictionary
private void dictAdd(int pointerId, GameObject obj)
{
pointerIdToGameObject.Add(pointerId, obj);
}
//Removes pointerId and its value from dictionary
private void dictRemove(int pointerId)
{
pointerIdToGameObject.Remove(pointerId);
}
public enum PointerEventType
{
Drag, Down, Up
}
}
Usage:
1.Attach the WholeScreenPointer script to an empty GameObject or the Camera.
2.To receive any event in the scene, simply implement IDragHandler, IPointerDownHandler, IPointerUpHandler in any script then call WholeScreenPointer.Instance.registerGameObject(this.gameObject); once. Any event from the screen will now be sent to that script. Don't forget to unregister in the OnDisable() function.
For example, attach Test to any GameObject you want to receive touch events:
public class Test : MonoBehaviour, IDragHandler, IPointerDownHandler, IPointerUpHandler
{
void Start()
{
//Register this GameObject so that it will receive events from WholeScreenPointer script
WholeScreenPointer.Instance.registerGameObject(this.gameObject);
}
public void OnDrag(PointerEventData eventData)
{
Debug.Log("Dragging: ");
}
public void OnPointerDown(PointerEventData eventData)
{
Debug.Log("Pointer Down: ");
}
public void OnPointerUp(PointerEventData eventData)
{
Debug.Log("Pointer Up: ");
}
void OnDisable()
{
WholeScreenPointer.Instance.unRegisterGameObject(this.gameObject);
}
}
NOTE:
You only need to call WholeScreenPointer.Instance.registerGameObject(this.gameObject); if you want to receive event anywhere on the screen. If you just want to receive event from current Object, then you don't have to call this. If you do, you will receive multiple events.
Other Important functions:
Enable WholeScreen Event - WholeScreenPointer.Instance.enablewholeScreenPointer(true);
Disable WholeScreen Event - WholeScreenPointer.Instance.enablewholeScreenPointer(false);
Finally, this can be improved more.
The question and the answer I am going to post seems pretty much opinion based. Nevertheless I am going to answer as best as I can.
If you are trying to detect pointer events on the screen, there is nothing wrong with representing the screen with an object. In your case, you use a 3D collider to cover the entire frustum of the camera. However, there is a native way to do this in Unity, using a 2D UI object that covers the entire screen. The screen can be best represented by a 2D object. For me, this seems like a natural way to do it.
I use a generic code for this purpose:
public class Screen : MonoSingleton<Screen>, IPointerClickHandler, IDragHandler, IBeginDragHandler, IEndDragHandler, IPointerDownHandler, IPointerUpHandler, IScrollHandler {
private bool holding = false;
private PointerEventData lastPointerEventData;
#region Events
public delegate void PointerEventHandler(PointerEventData data);
static public event PointerEventHandler OnPointerClick = delegate { };
static public event PointerEventHandler OnPointerDown = delegate { };
/// <summary> Dont use delta data as it will be wrong. If you are going to use delta, use OnDrag instead. </summary>
static public event PointerEventHandler OnPointerHold = delegate { };
static public event PointerEventHandler OnPointerUp = delegate { };
static public event PointerEventHandler OnBeginDrag = delegate { };
static public event PointerEventHandler OnDrag = delegate { };
static public event PointerEventHandler OnEndDrag = delegate { };
static public event PointerEventHandler OnScroll = delegate { };
#endregion
#region Interface Implementations
void IPointerClickHandler.OnPointerClick(PointerEventData e) {
lastPointerEventData = e;
OnPointerClick(e);
}
// And other interface implementations, you get the point
#endregion
void Update() {
if (holding) {
OnPointerHold(lastPointerEventData);
}
}
}
The Screen is a singleton, because there is only one screen in the context of the game. Objects(like camera) subscribe to its pointer events, and arrange theirselves accordingly. This also keeps single-responsibility intact.
You would use this as appending it to an object that represents the so called glass (surface of the screen). If you think buttons on the UI as popping out of the screen, glass would be under them. For this, the glass has to be the first child of the Canvas. Of course, the Canvas has to be rendered in screen space for it to make sense.
One hack here, which doesn't make sense is to add an invisible Image component to the glass, so it would receive events. This acts like the raycast target of the glass.
You could also use Input (Input.touches etc.) to implement this glass object. It would work as checking if the input changed in every Update call. This seems like a polling-based approach to me, whereas the above one is an event-based approach.
Your question seems as if looking for a way to justify using the Input class. IMHO, Do not make it harder for yourself. Use what works. And accept the fact that Unity is not perfect.

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