1 | using System;
|
---|
2 | using System.Collections;
|
---|
3 | using System.Collections.Generic;
|
---|
4 | using System.Security.Cryptography;
|
---|
5 | using System.Text;
|
---|
6 | using System.Threading;
|
---|
7 | using UnityEngine;
|
---|
8 | using UnityEngine.Events;
|
---|
9 | using UnityEngine.EventSystems;
|
---|
10 | using UnityEngine.SceneManagement;
|
---|
11 | using UnityEngine.UI;
|
---|
12 |
|
---|
13 | /**
|
---|
14 | * This class can be used to monitor the usage of an AR/VR. For this, it needs to be set as a component of
|
---|
15 | * a game object in a scene. There is a prefab coming with the script, that contains only the script itself.
|
---|
16 | * The script automatically searches the scene for objects of type Selectable, ScrollRect, and EventTrigger
|
---|
17 | * and registers with them for any event that they handle. So for example, for any button in the scene, the
|
---|
18 | * script registers for PointerClick events. If any of the registered events occurs, the method LogEvent()
|
---|
19 | * is called with respective parameters being the event type and the target. The event type is a string
|
---|
20 | * denoting which event took place. The event target is the game object on which the event was observed.
|
---|
21 | * The events are collected until a number of 10 events were observed or the scene is ending. Then the
|
---|
22 | * collected events are sent in JSON format to a monitoring server, which stores them. The server URL can
|
---|
23 | * be configured using the public class variable serverURL.
|
---|
24 | * The events are always connected to an appId and a clientId. This is required by the server to identify
|
---|
25 | * which events belong to which running instance of an AR/VR. The app id is basically the name of the app.
|
---|
26 | * It can be changes per instance to allow event separation for different instances of this script. The
|
---|
27 | * client id is a pseudo id calculated based on environment variables of the system on which the app is
|
---|
28 | * running. It cannot be used to trace back the individual user.
|
---|
29 | * The logging can also be triggered in a static fashion from any other script upon request. For this, the
|
---|
30 | * first instance of this class in the scene graph can be retrieved using the getInstance() method. Then
|
---|
31 | * on this instance, LogEvent() can be called with the above mentioned parameters. A call of this method
|
---|
32 | * is handled in the same manner as the other calls caused by the self registration of this class for any
|
---|
33 | * scene internal events.
|
---|
34 | * The searching for all objects in a scene for which the usage must be monitored is done continuously in
|
---|
35 | * form of a coroutine. The reason for this is, that the objects in a scene may change over time and the
|
---|
36 | * script cannot be notified about new objects of interest. Hence, the coroutine performs a kind of polling
|
---|
37 | * mechanism. This mechanism is implemented in a fashion so that not too much action happens in a frame. In
|
---|
38 | * addition, the coroutine prevents to continuously check the full scene graph at once for objects of
|
---|
39 | * interest but only sub graphs starting at the different root nodes in a scene. This may cause, that some
|
---|
40 | * events on objects of interest may be lost. But this will not be the default case as the whole scene will
|
---|
41 | * be reconsidered in only a few frames and afterwards, the coroutine will wait only a further second
|
---|
42 | * before it starts rescanning the scene.
|
---|
43 | * In two further coroutines, the class continously traces the movement and rotation of the main camera
|
---|
44 | * which is considered to be the head of the AR/VR user. It must be done in coroutines as there is no
|
---|
45 | * notification possibility on location changes for the camera. And in addition, the check can be spread
|
---|
46 | * over time so that it takes place only once per 100 ms. If the coroutines determine a change in location
|
---|
47 | * or rotation, they produce corresponding events. Location and rotation changes usually take some time.
|
---|
48 | * Hence, they are logged, when they are finished. If in the meantime, other events happen, they are
|
---|
49 | * logged before that. But this is no drawback, as the location and rotation changes are logged together
|
---|
50 | * with time stamps indicating when the movement or rotation started and ended.
|
---|
51 | */
|
---|
52 | public class AutoQUESTGenericMonitorUnity : MonoBehaviour
|
---|
53 | {
|
---|
54 | /** The instance of this script which is considered the one to be returned when calling getInstance() */
|
---|
55 | private static AutoQUESTGenericMonitorUnity instance;
|
---|
56 |
|
---|
57 | /** The id of the client, i.e., user, for which the actions are recorded (static, because it is the
|
---|
58 | * same for all instances of this script) */
|
---|
59 | private static string clientId;
|
---|
60 |
|
---|
61 | /** the registered event targets (static, because this map can become large and should, hence, be shared
|
---|
62 | * between different instances of this script) */
|
---|
63 | private static Dictionary<GameObject, string> eventTargetIds = new Dictionary<GameObject, string>();
|
---|
64 |
|
---|
65 | /** a lock for multiple reads but only single writes on the collected event target ids */
|
---|
66 | private static ReaderWriterLockSlim readWriteLockEventTargetIds = new ReaderWriterLockSlim();
|
---|
67 |
|
---|
68 | /** the current list of events per server collected so far (static, because multiple instances of this
|
---|
69 | * script may want to log to the same server and the events should always be sorted in the order in
|
---|
70 | * which they occurred) */
|
---|
71 | private static Dictionary<string, List<EventData>> collectedEvents = new Dictionary<string, List<EventData>>();
|
---|
72 |
|
---|
73 | /** a lock for multiple reads but only single writes on the collected event target ids */
|
---|
74 | private static ReaderWriterLockSlim readWriteLockCollectedEvents = new ReaderWriterLockSlim();
|
---|
75 |
|
---|
76 | /** The AutoQUEST generic monitor server to send the logged data to (non static to allow for multiple
|
---|
77 | * script instances logging different things to different servers and to be configurable via unity) */
|
---|
78 | public string serverURL = "https://swe-tooling.informatik.uni-goettingen.de/autoquest-genericmonitor/";
|
---|
79 |
|
---|
80 | /** The id of the application that the client uses (non static to allow for multiple script instances
|
---|
81 | * logging different things for different virtual apps and to be configurable via unity) */
|
---|
82 | public string appId;
|
---|
83 |
|
---|
84 | /**
|
---|
85 | * stores for any relevant game object the list of event listeners registered on it. This is required to
|
---|
86 | * remove them afterwards for clean up as removing can only be done for known listener objects
|
---|
87 | */
|
---|
88 | private Dictionary<UnityEngine.Object, Listeners> registeredUnityActions = new Dictionary<UnityEngine.Object, Listeners>();
|
---|
89 |
|
---|
90 | /** determine the client id */
|
---|
91 | static AutoQUESTGenericMonitorUnity()
|
---|
92 | {
|
---|
93 | // determine a client id
|
---|
94 | StringBuilder id = new StringBuilder();
|
---|
95 | foreach (DictionaryEntry de in Environment.GetEnvironmentVariables())
|
---|
96 | {
|
---|
97 | id.Append(de.Key);
|
---|
98 | id.Append(de.Value);
|
---|
99 | }
|
---|
100 |
|
---|
101 | clientId = toMD5Hash(id.ToString());
|
---|
102 | }
|
---|
103 |
|
---|
104 | /**
|
---|
105 | * similar to a singleton, returns the single instance of this script in the scene. If there are multiple instances of this
|
---|
106 | * script, the first one found is taken. If there is not instance of this script attached to any active object in the scene
|
---|
107 | * an exception is thrown.
|
---|
108 | */
|
---|
109 | public static AutoQUESTGenericMonitorUnity getInstance()
|
---|
110 | {
|
---|
111 | readWriteLockCollectedEvents.EnterUpgradeableReadLock();
|
---|
112 |
|
---|
113 | if (instance == null)
|
---|
114 | {
|
---|
115 | readWriteLockCollectedEvents.EnterWriteLock();
|
---|
116 | // search for the first instances of this script in active objects and take it as the single instance
|
---|
117 | instance = FindObjectOfType<AutoQUESTGenericMonitorUnity>();
|
---|
118 |
|
---|
119 | if (instance == null)
|
---|
120 | {
|
---|
121 | // there is no instance of this script belonging to an active object in this scene. Therefore, we throw an
|
---|
122 | // exception, as this is a prerequisite
|
---|
123 | throw new Exception("getInstance() must not be called without an instance of this script attached to an " +
|
---|
124 | "active object in the scene. This is a programming mistake and must be corrected. You can, e.g., " +
|
---|
125 | "add the AutoQUEST Monitor Prefab to the scene to solve this problem.");
|
---|
126 | }
|
---|
127 |
|
---|
128 | readWriteLockCollectedEvents.ExitWriteLock();
|
---|
129 | }
|
---|
130 |
|
---|
131 | readWriteLockCollectedEvents.ExitUpgradeableReadLock();
|
---|
132 | return instance;
|
---|
133 | }
|
---|
134 |
|
---|
135 | /**
|
---|
136 | * generates an app id if non is already set and starts all coroutines
|
---|
137 | */
|
---|
138 | void Start()
|
---|
139 | {
|
---|
140 | readWriteLockCollectedEvents.EnterWriteLock();
|
---|
141 |
|
---|
142 | // get an id for the application
|
---|
143 | if ((appId == null) || ("".Equals(appId)))
|
---|
144 | {
|
---|
145 | appId = Application.productName;
|
---|
146 |
|
---|
147 | if ((appId == null) || ("".Equals(appId)))
|
---|
148 | {
|
---|
149 | appId = Application.identifier;
|
---|
150 | }
|
---|
151 | }
|
---|
152 |
|
---|
153 | StartCoroutine("EventHandlingRegistrationCoroutine");
|
---|
154 | StartCoroutine("MovementTrackingCoroutine");
|
---|
155 | StartCoroutine("GazeTrackingCoroutine");
|
---|
156 |
|
---|
157 | readWriteLockCollectedEvents.ExitWriteLock();
|
---|
158 | }
|
---|
159 |
|
---|
160 | /**
|
---|
161 | * Sends all remaining collected events to the server
|
---|
162 | */
|
---|
163 | void OnApplicationQuit()
|
---|
164 | {
|
---|
165 | StopAllCoroutines();
|
---|
166 | SendEventsToServer(true);
|
---|
167 | }
|
---|
168 |
|
---|
169 | /**
|
---|
170 | * collects an event for a given game object and triggers sending events to the server, if sufficient events have been collected
|
---|
171 | */
|
---|
172 | public void LogEvent(string eventType, GameObject eventTarget, params KeyValuePair<String, String>[] parameters)
|
---|
173 | {
|
---|
174 | // determine the target
|
---|
175 | RegisterEventTarget(eventTarget);
|
---|
176 |
|
---|
177 | Debug.Log("logEvent called: " + eventType + " (" + GetEventTargetId(eventTarget) + ")");
|
---|
178 |
|
---|
179 | EventData eventData = new EventData
|
---|
180 | { type = eventType, target = eventTarget, timestamp = DateTime.UtcNow.Ticks,
|
---|
181 | x = (float) Math.Round(eventTarget.transform.position.x, 2),
|
---|
182 | y = (float) Math.Round(eventTarget.transform.position.y, 2),
|
---|
183 | z = (float) Math.Round(eventTarget.transform.position.z, 2),
|
---|
184 | parameters = parameters};
|
---|
185 |
|
---|
186 | readWriteLockCollectedEvents.EnterUpgradeableReadLock();
|
---|
187 | List<EventData> eventList;
|
---|
188 |
|
---|
189 | if (!collectedEvents.ContainsKey(serverURL))
|
---|
190 | {
|
---|
191 | eventList = new List<EventData>();
|
---|
192 | readWriteLockCollectedEvents.EnterWriteLock();
|
---|
193 | collectedEvents.Add(serverURL, eventList);
|
---|
194 | readWriteLockCollectedEvents.ExitWriteLock();
|
---|
195 | }
|
---|
196 | else
|
---|
197 | {
|
---|
198 | eventList = collectedEvents[serverURL];
|
---|
199 | }
|
---|
200 |
|
---|
201 | eventList.Add(eventData);
|
---|
202 |
|
---|
203 | if (eventList.Count >= 10)
|
---|
204 | {
|
---|
205 | SendEventsToServer(false);
|
---|
206 | }
|
---|
207 |
|
---|
208 | readWriteLockCollectedEvents.ExitUpgradeableReadLock();
|
---|
209 | }
|
---|
210 |
|
---|
211 | /**
|
---|
212 | * collects an event and triggers sending events to the server, if sufficient events have been collected.
|
---|
213 | * The event target considered for the given event is the game object of which the instance of this class
|
---|
214 | * is a component. Hence, this method must be called with care to ensure, that the correct correct
|
---|
215 | * event target is considered.
|
---|
216 | */
|
---|
217 | public void LogEvent(string eventType, params KeyValuePair<String, String>[] parameters)
|
---|
218 | {
|
---|
219 | LogEvent(eventType, this.gameObject, parameters);
|
---|
220 | }
|
---|
221 |
|
---|
222 | /**
|
---|
223 | * used for storing event targets and their hierarchies and for calculating their unique ids
|
---|
224 | */
|
---|
225 | private void RegisterEventTarget(GameObject eventTarget)
|
---|
226 | {
|
---|
227 | // check, if the target is already registered. If so, do nothing
|
---|
228 | readWriteLockEventTargetIds.EnterReadLock();
|
---|
229 |
|
---|
230 | if (eventTargetIds.ContainsKey(eventTarget))
|
---|
231 | {
|
---|
232 | readWriteLockEventTargetIds.ExitReadLock();
|
---|
233 | return;
|
---|
234 | }
|
---|
235 |
|
---|
236 | readWriteLockEventTargetIds.ExitReadLock();
|
---|
237 |
|
---|
238 | // before adding the target itself, check for the parents
|
---|
239 | GameObject parent = eventTarget;
|
---|
240 | string id = "";
|
---|
241 |
|
---|
242 | while (parent != null)
|
---|
243 | {
|
---|
244 | id += parent.name;
|
---|
245 | if (parent.transform.parent != null)
|
---|
246 | {
|
---|
247 | parent = parent.transform.parent.gameObject;
|
---|
248 | RegisterEventTarget(parent);
|
---|
249 | }
|
---|
250 | else
|
---|
251 | {
|
---|
252 | parent = null;
|
---|
253 | }
|
---|
254 | }
|
---|
255 |
|
---|
256 | // now, register the target. Ensure, that no other thread added it in the meantime
|
---|
257 | readWriteLockEventTargetIds.EnterUpgradeableReadLock();
|
---|
258 |
|
---|
259 | if (eventTargetIds.ContainsKey(eventTarget))
|
---|
260 | {
|
---|
261 | readWriteLockEventTargetIds.ExitUpgradeableReadLock();
|
---|
262 | return;
|
---|
263 | }
|
---|
264 |
|
---|
265 | id += eventTarget.scene.name;
|
---|
266 |
|
---|
267 | id = toMD5Hash(id);
|
---|
268 |
|
---|
269 | readWriteLockEventTargetIds.EnterWriteLock();
|
---|
270 | eventTargetIds.Add(eventTarget, id);
|
---|
271 | readWriteLockEventTargetIds.ExitWriteLock();
|
---|
272 |
|
---|
273 | Debug.Log("registerEventTarget called: " + eventTarget.name + " (" + id + ")");
|
---|
274 |
|
---|
275 | readWriteLockEventTargetIds.ExitUpgradeableReadLock();
|
---|
276 | }
|
---|
277 |
|
---|
278 | /**
|
---|
279 | * returns the id calculated for an event target for its identification
|
---|
280 | */
|
---|
281 | private string GetEventTargetId(GameObject eventTarget)
|
---|
282 | {
|
---|
283 | readWriteLockEventTargetIds.EnterReadLock();
|
---|
284 | String result = eventTargetIds[eventTarget];
|
---|
285 | readWriteLockEventTargetIds.ExitReadLock();
|
---|
286 |
|
---|
287 | return result;
|
---|
288 | }
|
---|
289 |
|
---|
290 | /**
|
---|
291 | * sends all events to the server
|
---|
292 | */
|
---|
293 | private void SendEventsToServer(Boolean wait)
|
---|
294 | {
|
---|
295 | EventData[] eventsToSend = null;
|
---|
296 |
|
---|
297 | // get a copy and directly release the lock so that further events can be collected in parallel.
|
---|
298 | readWriteLockCollectedEvents.EnterReadLock();
|
---|
299 |
|
---|
300 | if (collectedEvents.ContainsKey(serverURL)) {
|
---|
301 | eventsToSend = collectedEvents[serverURL].ToArray();
|
---|
302 | collectedEvents[serverURL].Clear();
|
---|
303 | }
|
---|
304 |
|
---|
305 | readWriteLockCollectedEvents.ExitReadLock();
|
---|
306 |
|
---|
307 | // send events only, if there are some
|
---|
308 | if ((eventsToSend == null) || (eventsToSend.Length <= 0))
|
---|
309 | {
|
---|
310 | return;
|
---|
311 | }
|
---|
312 |
|
---|
313 | Debug.Log("sending " + eventsToSend.Length + " events to the server");
|
---|
314 |
|
---|
315 | String message = CreateMessage(eventsToSend);
|
---|
316 |
|
---|
317 | WWW www = new WWW(serverURL, Encoding.UTF8.GetBytes(message));
|
---|
318 |
|
---|
319 | if (wait)
|
---|
320 | {
|
---|
321 | while (!www.isDone)
|
---|
322 | {
|
---|
323 | new WaitForSeconds(0.1f);
|
---|
324 | }
|
---|
325 | }
|
---|
326 | }
|
---|
327 |
|
---|
328 | /**
|
---|
329 | * creates a message containing the events and the corresponding targets
|
---|
330 | */
|
---|
331 | private string CreateMessage(EventData[] eventsToSend)
|
---|
332 | {
|
---|
333 | // create the message
|
---|
334 | StringBuilder message = new StringBuilder();
|
---|
335 | message.AppendLine("{");
|
---|
336 | message.AppendLine(" \"message\": {");
|
---|
337 | message.AppendLine(" \"clientInfos\": {");
|
---|
338 | message.AppendLine(" \"appId\": \"" + appId + "\"");
|
---|
339 | message.AppendLine(" \"clientId\": \"" + clientId + "\"");
|
---|
340 | message.AppendLine(" },");
|
---|
341 |
|
---|
342 | // determine the targets to be send to the server
|
---|
343 | Dictionary<GameObject, List<GameObject>> targetsToSend = new Dictionary<GameObject, List<GameObject>>();
|
---|
344 | List<GameObject> rootTargets = new List<GameObject>();
|
---|
345 |
|
---|
346 | for (int i = 0; i < eventsToSend.Length; i++)
|
---|
347 | {
|
---|
348 | GameObject targetToSend = eventsToSend[i].target;
|
---|
349 | GameObject lastChild = null;
|
---|
350 |
|
---|
351 | while (targetToSend != null)
|
---|
352 | {
|
---|
353 | List<GameObject> children = null;
|
---|
354 |
|
---|
355 | if (!targetsToSend.ContainsKey(targetToSend))
|
---|
356 | {
|
---|
357 | children = new List<GameObject>();
|
---|
358 | if (lastChild != null)
|
---|
359 | {
|
---|
360 | children.Add(lastChild);
|
---|
361 | }
|
---|
362 |
|
---|
363 | targetsToSend.Add(targetToSend, children);
|
---|
364 |
|
---|
365 | if (targetToSend.transform.parent != null)
|
---|
366 | {
|
---|
367 | lastChild = targetToSend;
|
---|
368 | targetToSend = targetToSend.transform.parent.gameObject;
|
---|
369 | }
|
---|
370 | else
|
---|
371 | {
|
---|
372 | rootTargets.Add(targetToSend);
|
---|
373 | targetToSend = null;
|
---|
374 | }
|
---|
375 | }
|
---|
376 | else
|
---|
377 | {
|
---|
378 | if (lastChild != null)
|
---|
379 | {
|
---|
380 | children = targetsToSend[targetToSend];
|
---|
381 | children.Add(lastChild);
|
---|
382 | }
|
---|
383 | targetToSend = null;
|
---|
384 | }
|
---|
385 | }
|
---|
386 | }
|
---|
387 |
|
---|
388 | // add the targets to be send to the server
|
---|
389 | message.AppendLine(" \"targetStructure\": [");
|
---|
390 | message.AppendLine(" {");
|
---|
391 | message.AppendLine(" \"targetId\": \"" + toMD5Hash(SceneManager.GetActiveScene().name) + "\",");
|
---|
392 | message.AppendLine(" \"name\": \"" + SceneManager.GetActiveScene().name + "\",");
|
---|
393 | message.AppendLine(" \"children\": [");
|
---|
394 |
|
---|
395 | foreach (GameObject root in rootTargets)
|
---|
396 | {
|
---|
397 | DumpTarget(message, root, targetsToSend, " ");
|
---|
398 | message.AppendLine(" ,");
|
---|
399 | }
|
---|
400 |
|
---|
401 | message.AppendLine(" ]");
|
---|
402 | message.AppendLine(" }");
|
---|
403 | message.AppendLine(" ],");
|
---|
404 |
|
---|
405 | // now add the events
|
---|
406 | message.AppendLine(" \"events\": [");
|
---|
407 |
|
---|
408 | for (int i = 0; i < eventsToSend.Length; i++)
|
---|
409 | {
|
---|
410 | message.AppendLine(" {");
|
---|
411 | message.AppendLine(" \"time\": \"" + eventsToSend[i].timestamp + "\",");
|
---|
412 | message.AppendLine(" \"type\": \"" + eventsToSend[i].type + "\",");
|
---|
413 | message.AppendLine(" \"targetId\": \"" + GetEventTargetId(eventsToSend[i].target) + "\",");
|
---|
414 | message.AppendLine(" \"targetPosition\": \"(" + eventsToSend[i].x + ", " + eventsToSend[i].y +
|
---|
415 | ", " + eventsToSend[i].z + ")\",");
|
---|
416 | if ((eventsToSend[i].parameters != null) && (eventsToSend[i].parameters.Length > 0))
|
---|
417 | {
|
---|
418 | foreach (KeyValuePair<string, string> parameter in eventsToSend[i].parameters)
|
---|
419 | {
|
---|
420 | message.AppendLine(" \"" + parameter.Key + "\": \"" + parameter.Value + "\",");
|
---|
421 | }
|
---|
422 | }
|
---|
423 |
|
---|
424 | message.AppendLine(" },");
|
---|
425 | }
|
---|
426 |
|
---|
427 | message.AppendLine(" ]");
|
---|
428 |
|
---|
429 | message.AppendLine(" }");
|
---|
430 | message.AppendLine("}");
|
---|
431 |
|
---|
432 | String result = message.ToString();
|
---|
433 | Debug.Log(result);
|
---|
434 |
|
---|
435 | return result;
|
---|
436 | }
|
---|
437 |
|
---|
438 | /**
|
---|
439 | * dumps a target to the given string builder by considering the indentation. Calls itself recursively to dump the target hierarchy.
|
---|
440 | */
|
---|
441 | private void DumpTarget(StringBuilder message, GameObject eventTarget, Dictionary<GameObject, List<GameObject>> targetsToSend, string indent)
|
---|
442 | {
|
---|
443 | message.Append(indent);
|
---|
444 | message.AppendLine("{");
|
---|
445 | message.Append(indent);
|
---|
446 | message.AppendLine(" \"targetId\": \"" + GetEventTargetId(eventTarget) + "\",");
|
---|
447 | message.Append(indent);
|
---|
448 | message.AppendLine(" \"name\": \"" + eventTarget.name + "\",");
|
---|
449 |
|
---|
450 | List<GameObject> children = targetsToSend[eventTarget];
|
---|
451 |
|
---|
452 | if (children.Count > 0)
|
---|
453 | {
|
---|
454 | message.Append(indent);
|
---|
455 | message.AppendLine(" \"children\": [");
|
---|
456 |
|
---|
457 | foreach(GameObject child in children)
|
---|
458 | {
|
---|
459 | DumpTarget(message, child, targetsToSend, indent + " ");
|
---|
460 | message.Append(indent);
|
---|
461 | message.AppendLine(" ,");
|
---|
462 | }
|
---|
463 |
|
---|
464 | message.Append(indent);
|
---|
465 | message.AppendLine(" ]");
|
---|
466 | }
|
---|
467 |
|
---|
468 | message.Append(indent);
|
---|
469 | message.AppendLine("}");
|
---|
470 | }
|
---|
471 |
|
---|
472 | /**
|
---|
473 | * convenience method to calculate MD5 hashes for a given string
|
---|
474 | */
|
---|
475 | private static string toMD5Hash(string str)
|
---|
476 | {
|
---|
477 | MD5 md5Algorithm = new MD5CryptoServiceProvider();
|
---|
478 | byte[] strAsBytes = Encoding.UTF8.GetBytes(str);
|
---|
479 | strAsBytes = md5Algorithm.ComputeHash(strAsBytes);
|
---|
480 |
|
---|
481 | return BitConverter.ToString(strAsBytes).Replace("-", "");
|
---|
482 | }
|
---|
483 |
|
---|
484 |
|
---|
485 | /**
|
---|
486 | * In this coroutine, we continuously check all game objects in the scene for changes so that we can register
|
---|
487 | * event listeners for objects on which they should exist. To not cause too much effort for that, the coroutine yields from
|
---|
488 | * time to time. The event listeners are created only once and then stored in a dictionary. This is required as event listeners
|
---|
489 | * can only be removed using their reference. But we need to be able to remove them to ensure that they are registered only
|
---|
490 | * once per object.
|
---|
491 | * After some time, the objects on which the event listeners are registers may be destroyed. Hence, the dictionary may
|
---|
492 | * contain event listeners for objects that do not exist anymore. To handle this, the coroutine also stores the last time
|
---|
493 | * since when the event listeners have been considered last during a registration cycle of this coroutine. If this point
|
---|
494 | * in time is more than 5 seconds ago, the coroutine considers the object to be destroyed and, therefore, also destroys
|
---|
495 | * the event listeners.
|
---|
496 | */
|
---|
497 | private IEnumerator EventHandlingRegistrationCoroutine()
|
---|
498 | {
|
---|
499 | List<GameObject> currentRootGameObjects = new List<GameObject>(100);
|
---|
500 | int rootGameObjectsPointer = 0;
|
---|
501 | LinkedList<UnityEngine.Object> currentRelevantGameObjects = new LinkedList<UnityEngine.Object>();
|
---|
502 | int gameObjectsHandledPerFrame = 10;
|
---|
503 |
|
---|
504 | while (true)
|
---|
505 | {
|
---|
506 | if (rootGameObjectsPointer > (currentRootGameObjects.Count - 1))
|
---|
507 | {
|
---|
508 | // we need to reread the root objects of all loaded scenes
|
---|
509 | for (int i = 0; i < SceneManager.sceneCount; i++)
|
---|
510 | {
|
---|
511 | Scene scene = SceneManager.GetSceneAt(i);
|
---|
512 | if (scene.isLoaded)
|
---|
513 | {
|
---|
514 | currentRootGameObjects.AddRange(scene.GetRootGameObjects());
|
---|
515 | }
|
---|
516 | }
|
---|
517 |
|
---|
518 | rootGameObjectsPointer = 0;
|
---|
519 |
|
---|
520 | // yield and continue in next frame
|
---|
521 | yield return null;
|
---|
522 | //print("considering " + currentRootGameObjects.Count + " root objects");
|
---|
523 | }
|
---|
524 |
|
---|
525 | // check, if there are enough game objects to be processed in the current frame
|
---|
526 | while ((currentRelevantGameObjects.Count < gameObjectsHandledPerFrame) &&
|
---|
527 | (rootGameObjectsPointer < currentRootGameObjects.Count))
|
---|
528 | {
|
---|
529 | // we need to read some further game objects into the list of those to be handled next
|
---|
530 | addRelevantGameObjects(currentRootGameObjects[rootGameObjectsPointer++], currentRelevantGameObjects);
|
---|
531 | yield return null;
|
---|
532 | }
|
---|
533 |
|
---|
534 | //print("considering next " + currentRelevantGameObjects.Count + " game objects for which actions must be logged");
|
---|
535 |
|
---|
536 | // now handle the relevant objects, but at most gameObjectsHandledPerFrame at once
|
---|
537 | readWriteLockCollectedEvents.EnterWriteLock();
|
---|
538 | int currentlyHandled = 0;
|
---|
539 | while (currentRelevantGameObjects.Count > 0)
|
---|
540 | {
|
---|
541 | UnityEngine.Object relevantObject = currentRelevantGameObjects.First.Value;
|
---|
542 |
|
---|
543 | currentRelevantGameObjects.RemoveFirst();
|
---|
544 | //print("ensuring event handling for " + relevantObject);
|
---|
545 | ensureEventHandling(relevantObject);
|
---|
546 | currentlyHandled++;
|
---|
547 |
|
---|
548 | if (currentlyHandled % gameObjectsHandledPerFrame == 0)
|
---|
549 | {
|
---|
550 | // we handled enough relevant game objects in this frame. Release the lock, yield, and continue in next frame
|
---|
551 | // by obtaining the lock again
|
---|
552 | readWriteLockCollectedEvents.ExitWriteLock();
|
---|
553 | yield return null;
|
---|
554 | readWriteLockCollectedEvents.EnterWriteLock();
|
---|
555 | }
|
---|
556 | }
|
---|
557 | readWriteLockCollectedEvents.ExitWriteLock();
|
---|
558 |
|
---|
559 | yield return null;
|
---|
560 |
|
---|
561 | readWriteLockCollectedEvents.EnterWriteLock();
|
---|
562 | // now check for any listener in the dictionary, if it was touched in the last cycles, and if not, destroy them
|
---|
563 | long thresholdTime = DateTime.UtcNow.Ticks - 50000000;
|
---|
564 | List<UnityEngine.Object> obsoleteEntries = new List<UnityEngine.Object>();
|
---|
565 | foreach (KeyValuePair<UnityEngine.Object, Listeners> entry in registeredUnityActions)
|
---|
566 | {
|
---|
567 | if (entry.Value.lastTouched < thresholdTime)
|
---|
568 | {
|
---|
569 | obsoleteEntries.Add(entry.Key);
|
---|
570 | }
|
---|
571 | }
|
---|
572 |
|
---|
573 | foreach (UnityEngine.Object obsoleteEntry in obsoleteEntries)
|
---|
574 | {
|
---|
575 | registeredUnityActions.Remove(obsoleteEntry);
|
---|
576 | }
|
---|
577 | readWriteLockCollectedEvents.ExitWriteLock();
|
---|
578 |
|
---|
579 | if (rootGameObjectsPointer > (currentRootGameObjects.Count - 1))
|
---|
580 | {
|
---|
581 | // we now checked all relevant objects. Let's wait a second before starting the next registration cycle
|
---|
582 | yield return new WaitForSeconds(1f);
|
---|
583 | //yield return null;
|
---|
584 | }
|
---|
585 | else
|
---|
586 | {
|
---|
587 | yield return null;
|
---|
588 | }
|
---|
589 | }
|
---|
590 | }
|
---|
591 |
|
---|
592 | /**
|
---|
593 | * Traverses a hierarchy of game objects and adds game objects relevant for event logging to the given list
|
---|
594 | */
|
---|
595 | virtual protected void addRelevantGameObjects(GameObject gameObject, LinkedList<UnityEngine.Object> currentRelevantGameObjects)
|
---|
596 | {
|
---|
597 | if (gameObject)
|
---|
598 | {
|
---|
599 | Component[] selectables = gameObject.GetComponentsInChildren(typeof(Selectable));
|
---|
600 | Component[] scrollRects = gameObject.GetComponentsInChildren(typeof(ScrollRect));
|
---|
601 | Component[] eventTriggers = gameObject.GetComponentsInChildren(typeof(EventTrigger));
|
---|
602 |
|
---|
603 | foreach (Component relevantObject in selectables)
|
---|
604 | {
|
---|
605 | currentRelevantGameObjects.AddLast(relevantObject);
|
---|
606 | }
|
---|
607 |
|
---|
608 | foreach (Component relevantObject in scrollRects)
|
---|
609 | {
|
---|
610 | currentRelevantGameObjects.AddLast(relevantObject);
|
---|
611 | }
|
---|
612 |
|
---|
613 | foreach (Component relevantObject in eventTriggers)
|
---|
614 | {
|
---|
615 | currentRelevantGameObjects.AddLast(relevantObject);
|
---|
616 | }
|
---|
617 | }
|
---|
618 | }
|
---|
619 |
|
---|
620 | /**
|
---|
621 | * registers event handling for the given relevant object
|
---|
622 | */
|
---|
623 | virtual protected void ensureEventHandling(UnityEngine.Object relevantObject)
|
---|
624 | {
|
---|
625 | if (relevantObject is Button)
|
---|
626 | {
|
---|
627 | ensureEventHandling(((Button)relevantObject).onClick, "PointerClick", ((Button)relevantObject).gameObject);
|
---|
628 | }
|
---|
629 | else if (relevantObject is Toggle)
|
---|
630 | {
|
---|
631 | ensureEventHandling(((Toggle)relevantObject).onValueChanged, "ValueChanged", ((Toggle)relevantObject).gameObject);
|
---|
632 | }
|
---|
633 | else if (relevantObject is Slider)
|
---|
634 | {
|
---|
635 | ensureEventHandling(((Slider)relevantObject).onValueChanged, "ValueChanged", ((Slider)relevantObject).gameObject);
|
---|
636 | }
|
---|
637 | else if (relevantObject is Scrollbar)
|
---|
638 | {
|
---|
639 | ensureEventHandling(((Scrollbar)relevantObject).onValueChanged, "ValueChanged", ((Scrollbar)relevantObject).gameObject);
|
---|
640 | }
|
---|
641 | else if (relevantObject is Dropdown)
|
---|
642 | {
|
---|
643 | ensureEventHandling(((Dropdown)relevantObject).onValueChanged, "ValueChanged", ((Dropdown)relevantObject).gameObject);
|
---|
644 | }
|
---|
645 | else if (relevantObject is InputField)
|
---|
646 | {
|
---|
647 | ensureEventHandling(((InputField)relevantObject).onValueChanged, "ValueChanged", ((InputField)relevantObject).gameObject);
|
---|
648 | ensureEventHandling(((InputField)relevantObject).onEndEdit, "EditingEnded", ((InputField)relevantObject).gameObject);
|
---|
649 | }
|
---|
650 | else if (relevantObject is ScrollRect)
|
---|
651 | {
|
---|
652 | ensureEventHandling(((ScrollRect)relevantObject).onValueChanged, "ValueChanged", ((ScrollRect)relevantObject).gameObject);
|
---|
653 | }
|
---|
654 | else if (relevantObject is EventTrigger)
|
---|
655 | {
|
---|
656 | foreach (EventTrigger.Entry entry in ((EventTrigger)relevantObject).triggers)
|
---|
657 | {
|
---|
658 | ensureEventHandling(entry.callback, entry.eventID.ToString(), ((EventTrigger)relevantObject).gameObject);
|
---|
659 | }
|
---|
660 | }
|
---|
661 | else
|
---|
662 | {
|
---|
663 | Debug.Log("unknown type of relevant object: " + relevantObject.GetType());
|
---|
664 | }
|
---|
665 | }
|
---|
666 |
|
---|
667 | /**
|
---|
668 | * ensures that there is a listener for the given event on the given game object
|
---|
669 | */
|
---|
670 | private void ensureEventHandling(UnityEvent unityEvent, string eventType, GameObject eventTarget)
|
---|
671 | {
|
---|
672 | UnityAction listener = getListeners(eventTarget).getCreateListener(eventType, eventTarget);
|
---|
673 |
|
---|
674 | // remove the listener first to ensure, it is not registered twice
|
---|
675 | unityEvent.RemoveListener(listener);
|
---|
676 | unityEvent.AddListener(listener);
|
---|
677 | }
|
---|
678 |
|
---|
679 | /**
|
---|
680 | * ensures that there is a listener for the given event on the given game object
|
---|
681 | */
|
---|
682 | private void ensureEventHandling<T>(UnityEvent<T> unityEvent, string eventType, GameObject eventTarget)
|
---|
683 | {
|
---|
684 | UnityAction<T> listener = getListeners(eventTarget).getCreateListener<T>(eventType, eventTarget);
|
---|
685 |
|
---|
686 | // remove the listener first to ensure, it is not registered twice
|
---|
687 | unityEvent.RemoveListener(listener);
|
---|
688 | unityEvent.AddListener(listener);
|
---|
689 | }
|
---|
690 |
|
---|
691 | /**
|
---|
692 | * Determines the event listeners for a given eventTarget
|
---|
693 | */
|
---|
694 | protected Listeners getListeners(GameObject eventTarget)
|
---|
695 | {
|
---|
696 | Listeners listeners = null;
|
---|
697 |
|
---|
698 | registeredUnityActions.TryGetValue(eventTarget, out listeners);
|
---|
699 |
|
---|
700 | if (listeners == null)
|
---|
701 | {
|
---|
702 | listeners = createListeners();
|
---|
703 | registeredUnityActions.Add(eventTarget, listeners);
|
---|
704 | }
|
---|
705 |
|
---|
706 | return listeners;
|
---|
707 | }
|
---|
708 |
|
---|
709 | /**
|
---|
710 | * used to allow subclasses to create other, more concrete types of listeners
|
---|
711 | */
|
---|
712 | virtual protected Listeners createListeners()
|
---|
713 | {
|
---|
714 | return new Listeners(LogEvent);
|
---|
715 | }
|
---|
716 |
|
---|
717 | /**
|
---|
718 | * In this coroutine, we continuously check the position of the main camera to do movement tracking.
|
---|
719 | * For this the coroutine runs every 100 ms and compares the preceding location with the current one.
|
---|
720 | * If their is a change, then a motion is considered. If there is no change, then no motion is
|
---|
721 | * considered. If before there was a motion, then an event is logged. If the motion continues but
|
---|
722 | * the direction vectors of the previous and the current motion are more different than 5°, also
|
---|
723 | * an event is logged for the first motion.
|
---|
724 | */
|
---|
725 | private IEnumerator MovementTrackingCoroutine()
|
---|
726 | {
|
---|
727 | Vector3 previousPosition = Camera.main.transform.position;
|
---|
728 | Vector3 currentTranslationVector = new Vector3(0, 0, 0);
|
---|
729 | bool headIsMoving = false;
|
---|
730 | long timestamp = 0;
|
---|
731 |
|
---|
732 | while (true)
|
---|
733 | {
|
---|
734 | Vector3 currentPosition = Camera.main.transform.position;
|
---|
735 | Vector3 translationVector = currentPosition - previousPosition;
|
---|
736 |
|
---|
737 | if (translationVector.magnitude > 0.05)
|
---|
738 | {
|
---|
739 | // movement detected (position change larger than 5cm). Check if we are already in a movement
|
---|
740 | if (headIsMoving)
|
---|
741 | {
|
---|
742 | // the movement is continuing. Check, if it is almost the same direction
|
---|
743 | if (Vector3.Angle(currentTranslationVector, translationVector) < 5)
|
---|
744 | {
|
---|
745 | // we are still almost in the same direction. Extend the current translation vector
|
---|
746 | // and continue
|
---|
747 | currentTranslationVector += translationVector;
|
---|
748 | }
|
---|
749 | else
|
---|
750 | {
|
---|
751 | // the movement changed the direction. Therefore, log an event for the previous movement
|
---|
752 | // and set everything as if the new movement just started
|
---|
753 | Vector3 source = previousPosition - currentTranslationVector;
|
---|
754 | roundVector(ref source);
|
---|
755 | Vector3 dest = previousPosition;
|
---|
756 | roundVector(ref dest);
|
---|
757 |
|
---|
758 | LogEvent("headMoved", Camera.main.gameObject,
|
---|
759 | new KeyValuePair<string, string>("movementStart", timestamp.ToString()),
|
---|
760 | new KeyValuePair<string, string>("movementStop", DateTime.UtcNow.Ticks.ToString()),
|
---|
761 | new KeyValuePair<string, string>("from", "(" + source.x + ", " + source.y +
|
---|
762 | ", " + source.z + ")"),
|
---|
763 | new KeyValuePair<string, string>("to", "(" + dest.x + ", " + dest.y +
|
---|
764 | ", " + dest.z + ")"));
|
---|
765 |
|
---|
766 | currentTranslationVector = translationVector;
|
---|
767 | timestamp = DateTime.UtcNow.Ticks;
|
---|
768 | }
|
---|
769 | }
|
---|
770 | else
|
---|
771 | {
|
---|
772 | // head is started moving--> store the translation vector, set the flag, and continue
|
---|
773 | currentTranslationVector = translationVector;
|
---|
774 | headIsMoving = true;
|
---|
775 | timestamp = DateTime.UtcNow.Ticks;
|
---|
776 | }
|
---|
777 | }
|
---|
778 | else
|
---|
779 | {
|
---|
780 | if (headIsMoving)
|
---|
781 | {
|
---|
782 | // the movement of the head stopped --> log event and store that movement stopped
|
---|
783 | Vector3 source = previousPosition - currentTranslationVector;
|
---|
784 | roundVector(ref source);
|
---|
785 | Vector3 dest = previousPosition;
|
---|
786 | roundVector(ref dest);
|
---|
787 |
|
---|
788 | LogEvent("headMoved", Camera.main.gameObject,
|
---|
789 | new KeyValuePair<string, string>("movementStart", timestamp.ToString()),
|
---|
790 | new KeyValuePair<string, string>("movementStop", DateTime.UtcNow.Ticks.ToString()),
|
---|
791 | new KeyValuePair<string, string>("from", "(" + source.x + ", " + source.y +
|
---|
792 | ", " + source.z + ")"),
|
---|
793 | new KeyValuePair<string, string>("to", "(" + dest.x + ", " + dest.y +
|
---|
794 | ", " + dest.z + ")"));
|
---|
795 |
|
---|
796 | currentTranslationVector = new Vector3(0, 0, 0);
|
---|
797 | headIsMoving = false;
|
---|
798 | timestamp = 0;
|
---|
799 | }
|
---|
800 | // else no movement. Do nothing.
|
---|
801 | }
|
---|
802 |
|
---|
803 | previousPosition = currentPosition;
|
---|
804 |
|
---|
805 | // wait for the next execution
|
---|
806 | yield return new WaitForSeconds(0.1f);
|
---|
807 | }
|
---|
808 | }
|
---|
809 |
|
---|
810 | /**
|
---|
811 | * In this coroutine, we continuously check the rotation of the main camera to do rotation tracking.
|
---|
812 | * For this the coroutine runs every 100 ms and compares the preceding rotation with the current one.
|
---|
813 | * If their is a change, then a rotation is considered. If there is no change, then no rotation is
|
---|
814 | * considered. If before there was a rotation, then an event is logged.
|
---|
815 | */
|
---|
816 | private IEnumerator GazeTrackingCoroutine()
|
---|
817 | {
|
---|
818 | Vector3 initialOrientation = new Vector3(0, 0, 0);
|
---|
819 | Vector3 previousOrientation = Camera.main.transform.forward;
|
---|
820 | float currentRotationAngle = 0f;
|
---|
821 | bool headIsRotating = false;
|
---|
822 | long timestamp = 0;
|
---|
823 |
|
---|
824 | while (true)
|
---|
825 | {
|
---|
826 | Vector3 currentOrientation = Camera.main.transform.forward;
|
---|
827 | float rotationAngle = Vector3.Angle(previousOrientation, currentOrientation);
|
---|
828 |
|
---|
829 | if (rotationAngle > 2)
|
---|
830 | {
|
---|
831 | // rotation detected (gaze direction change larger than 5°). Check if we are already in a rotation
|
---|
832 | if (headIsRotating)
|
---|
833 | {
|
---|
834 | // the rotation is continuing. Extend the current rotation angle and continue
|
---|
835 | currentRotationAngle += rotationAngle;
|
---|
836 | }
|
---|
837 | else
|
---|
838 | {
|
---|
839 | // head has started rotating--> store the roation angle, set the flag, and continue
|
---|
840 | initialOrientation = previousOrientation;
|
---|
841 | currentRotationAngle = rotationAngle;
|
---|
842 | headIsRotating = true;
|
---|
843 | timestamp = DateTime.UtcNow.Ticks;
|
---|
844 | }
|
---|
845 | }
|
---|
846 | else
|
---|
847 | {
|
---|
848 | if (headIsRotating)
|
---|
849 | {
|
---|
850 | // the rotation of the head stopped --> log event and store that rotation stopped
|
---|
851 | Vector3 source = initialOrientation;
|
---|
852 | roundVector(ref source);
|
---|
853 | Vector3 dest = currentOrientation;
|
---|
854 | roundVector(ref dest);
|
---|
855 |
|
---|
856 | LogEvent("headRotated", Camera.main.gameObject,
|
---|
857 | new KeyValuePair<string, string>("rotationStart", timestamp.ToString()),
|
---|
858 | new KeyValuePair<string, string>("rotationStop", DateTime.UtcNow.Ticks.ToString()),
|
---|
859 | new KeyValuePair<string, string>("from", "(" + source.x + ", " + source.y +
|
---|
860 | ", " + source.z + ")"),
|
---|
861 | new KeyValuePair<string, string>("to", "(" + dest.x + ", " + dest.y +
|
---|
862 | ", " + dest.z + ")"));
|
---|
863 |
|
---|
864 | initialOrientation = new Vector3(0, 0, 0);
|
---|
865 | currentRotationAngle = 0;
|
---|
866 | headIsRotating = false;
|
---|
867 | timestamp = 0;
|
---|
868 | }
|
---|
869 | // else no movement. Do nothing.
|
---|
870 | }
|
---|
871 |
|
---|
872 | previousOrientation = currentOrientation;
|
---|
873 |
|
---|
874 | // wait for the next execution
|
---|
875 | yield return new WaitForSeconds(0.1f);
|
---|
876 | }
|
---|
877 | }
|
---|
878 |
|
---|
879 | /**
|
---|
880 | * convenience method to round the values of a vector to two decimal positions
|
---|
881 | */
|
---|
882 | private void roundVector(ref Vector3 vector)
|
---|
883 | {
|
---|
884 | vector.x = (float) Math.Round(vector.x, 2);
|
---|
885 | vector.y = (float) Math.Round(vector.y, 2);
|
---|
886 | vector.z = (float) Math.Round(vector.z, 2);
|
---|
887 | }
|
---|
888 |
|
---|
889 | /**
|
---|
890 | * inner structure to use for storing event data
|
---|
891 | */
|
---|
892 | private struct EventData
|
---|
893 | {
|
---|
894 | public string type;
|
---|
895 | public GameObject target;
|
---|
896 | public long timestamp;
|
---|
897 | public float x;
|
---|
898 | public float y;
|
---|
899 | public float z;
|
---|
900 | public KeyValuePair<string, string>[] parameters;
|
---|
901 | }
|
---|
902 |
|
---|
903 | /**
|
---|
904 | * inner structure to use for storing listeners for scene objects
|
---|
905 | */
|
---|
906 | protected class Listeners
|
---|
907 | {
|
---|
908 | /** the delegate for callback methods to call when events occur */
|
---|
909 | public delegate void EventHandlingCallback(string eventType, GameObject eventTarget, params KeyValuePair<String, String>[] parameters);
|
---|
910 |
|
---|
911 | /** stores when this information has been touched the last time */
|
---|
912 | internal long lastTouched = DateTime.UtcNow.Ticks;
|
---|
913 |
|
---|
914 | /** stores the list of listener managed in this structure */
|
---|
915 | protected List<KeyValuePair<string, object>> listeners = new List<KeyValuePair<string, object>>();
|
---|
916 |
|
---|
917 | /** the callback method to call if an event occurs */
|
---|
918 | protected EventHandlingCallback callback;
|
---|
919 |
|
---|
920 | /**
|
---|
921 | * used to pass the event handling callback method
|
---|
922 | */
|
---|
923 | public Listeners(EventHandlingCallback callback)
|
---|
924 | {
|
---|
925 | this.callback = callback;
|
---|
926 | }
|
---|
927 |
|
---|
928 | /**
|
---|
929 | * returns a listener for the given event type and updates the internal timestamp. If there is no listener
|
---|
930 | * one will be created and registered.
|
---|
931 | */
|
---|
932 | internal UnityAction getCreateListener(string eventType, GameObject eventTarget)
|
---|
933 | {
|
---|
934 | UnityAction listener = null;
|
---|
935 |
|
---|
936 | foreach (KeyValuePair<string, object> candidate in listeners)
|
---|
937 | {
|
---|
938 | if (candidate.Key == eventType)
|
---|
939 | {
|
---|
940 | listener = (UnityAction)candidate.Value;
|
---|
941 | break;
|
---|
942 | }
|
---|
943 | }
|
---|
944 |
|
---|
945 | if (listener == null)
|
---|
946 | {
|
---|
947 | listener = delegate { callback(eventType, eventTarget); };
|
---|
948 | addListener(eventType, listener);
|
---|
949 | //Debug.Log("ensuring handling of event " + eventType + " for " + eventTarget);
|
---|
950 | }
|
---|
951 |
|
---|
952 | lastTouched = DateTime.UtcNow.Ticks;
|
---|
953 | return listener;
|
---|
954 | }
|
---|
955 |
|
---|
956 | /**
|
---|
957 | * returns a listener for the given event type and updates the internal timestamp. If there is no listener
|
---|
958 | * one will be created and registered.
|
---|
959 | */
|
---|
960 | internal UnityAction<T> getCreateListener<T>(string eventType, GameObject eventTarget)
|
---|
961 | {
|
---|
962 | UnityAction<T> listener = null;
|
---|
963 |
|
---|
964 | foreach (KeyValuePair<string, object> candidate in listeners)
|
---|
965 | {
|
---|
966 | if (candidate.Key == eventType)
|
---|
967 | {
|
---|
968 | listener = (UnityAction<T>)candidate.Value;
|
---|
969 | break;
|
---|
970 | }
|
---|
971 | }
|
---|
972 |
|
---|
973 | if (listener == null)
|
---|
974 | {
|
---|
975 | listener = delegate (T arg)
|
---|
976 | {
|
---|
977 | if (arg != null)
|
---|
978 | {
|
---|
979 | if (arg.Equals(eventTarget))
|
---|
980 | {
|
---|
981 | callback(eventType, eventTarget);
|
---|
982 | }
|
---|
983 | else if (arg is PointerEventData)
|
---|
984 | {
|
---|
985 | PointerEventData eventData = arg as PointerEventData;
|
---|
986 | callback(eventType, eventTarget,
|
---|
987 | new KeyValuePair<string, string>("distance", eventData.pointerCurrentRaycast.distance.ToString()));
|
---|
988 | }
|
---|
989 | else
|
---|
990 | {
|
---|
991 | callback(eventType, eventTarget, new KeyValuePair<string, string>("value", arg.ToString()));
|
---|
992 | }
|
---|
993 | }
|
---|
994 | else
|
---|
995 | {
|
---|
996 | callback(eventType, eventTarget);
|
---|
997 | }
|
---|
998 | };
|
---|
999 |
|
---|
1000 | addListener(eventType, listener);
|
---|
1001 | //Debug.Log("ensuring handling of event " + eventType + " for " + eventTarget);
|
---|
1002 | }
|
---|
1003 |
|
---|
1004 | lastTouched = DateTime.UtcNow.Ticks;
|
---|
1005 | return listener;
|
---|
1006 | }
|
---|
1007 |
|
---|
1008 | /**
|
---|
1009 | * Adds a listener to the internal listener list while also updating the timestamp
|
---|
1010 | */
|
---|
1011 | internal void addListener(string eventType, object listener)
|
---|
1012 | {
|
---|
1013 | listeners.Add(new KeyValuePair<string, object>(eventType, listener));
|
---|
1014 | lastTouched = DateTime.UtcNow.Ticks;
|
---|
1015 | }
|
---|
1016 | }
|
---|
1017 |
|
---|
1018 | }
|
---|
1019 | |
---|