Thursday, December 21, 2017

Latest Update For Particularly Wavy

hey all,

It's been a while since my last post here. Part of the delay has been due to me catching a cold, and the rest is just that I've been too busy coding to actually write or talk about what I've been coding. One thing that is shown in the video is really smooth detection of hits: in the very first version of the game, I was performing raycasts every frame and based on those results, deciding if I needed to recalculate the lights, which as you might imagine leads to lots of frames where I perform the raycast and decide to do nothing.

In the latest version, I have delegates and events on every object that moves, rotates, or changes size, and when they do one of those things, the laser receives a message letting it know that something has moved, so it can then update the light positions and angles.



The problem was that when the light hit something, I was starting certain coroutines that would spam a message to the hit object once every frame, and in the code for the hit object I was running a bunch of checks inside the Update() function, which is run once every frame. Why would I do something stupid like that? Well, say you perform your calculation and you determine that light ray A is now hitting object O. You set the hit object variable of light ray A to object O and go on calculating what other results fall out from that. But what if on the previous frame light ray A was hitting object T, and object T is the target? In my game, I have a flag set inside the Target.cs script so that it knows when it is hit and what it is hit by. How do I tell the Target that it is no longer hit? That is the reason for the coroutines and Update() code: if the Target stopped receiving the message of being hit from the coroutine, which is stopped whenever I update the light anyway, then the Target can act correctly.

If you look at the code below, however, you will notice a different technique. I have a private variable called hitObject, and I have a public accessor for this called HitObject. Inside the setter, I run comparisons between the incoming value and the previous value of hitObject. Based on those, I send messages using Unity's built-in messaging system. These messages let the object know that it is no longer hit by or has just been hit by a particular light ray, as the case may be. Problem solved: no messy coroutines that need to be started or stopped, no code running every frame inside of Update() (OK, OK, no code besides the shader material updates) and using up CPU time. When something changes, the messages are sent and if nothing has changed, then nothing needs to be updated or checked.


using System.Collections.Generic;
using UnityEngine;

//[RequireComponent(typeof(CapsuleCollider))]
public class RayNode : MonoBehaviour
{
 private GameObject hitObject;
 public List < RayNode > children;
 public LineRenderer rayLR;
 public int depth;
 public bool influencedByBlackHole;
 public bool intensified;
 public GameObject metaball;

 public GameObject HitObject
 {
  get { return hitObject; }
  set
  {
   if ((value == null && hitObject != null ))
   {
    hitObject.SendMessage("OnLightExit", gameObject, 
SendMessageOptions.DontRequireReceiver);
    hitObject = null;
   }
   else if ((value != null && hitObject != value && hitObject != null ))
   {
    hitObject.SendMessage("OnLightExit", gameObject, 
SendMessageOptions.DontRequireReceiver);
    hitObject = value;
    hitObject.SendMessage("OnLightEnter", gameObject, 
SendMessageOptions.DontRequireReceiver);
   }
   else if ((hitObject == null && value != null))
   {
    hitObject = value;
    hitObject.SendMessage("OnLightEnter", gameObject,
SendMessageOptions.DontRequireReceiver);
   }
   else
   {
    hitObject = value;
   }
  }
 }

 float offset;

 private void Start()
 {
 }

 private void Update()
 {
  offset -= Time.deltaTime * 2f;
  if (offset < -.5f)
  {
   offset += .5f;
  }
  rayLR.materials[1].SetTextureOffset("_MainTex", 
new Vector2(offset / 4f, 0));
  rayLR.materials[2].SetTextureOffset("_MainTex", 
new Vector2(offset, 0));

 }


 public RayNode()
 {
  children = new List < RayNode > ();
 }

 public RayNode(GameObject RO)
 {
  hitObject = RO;
  children = new List < RayNode > ();
 }

 public void PruneSubTree(int childIndex)
 {
  //validate index
  if (childIndex > -1 && childIndex < children.Count)
  {
   RayNode toPrune = children[childIndex];
   //set the hitObject to null
   if (toPrune != null)
   {
    toPrune.HitObject = null;
   }
   

   List < RayNode > ch = new List < RayNode > ();
   for (int i = 0; i < toPrune.children.Count; i++)
   {
    ch.Add(toPrune.children[i]);
   }

   //reverse order is important in order to prevent skipping errors
   for (int i = ch.Count - 1; i > -1; i--)
   {
    toPrune.PruneSubTree(i);
   }

   if (toPrune != null)
   {
    Destroy(toPrune.gameObject, 0.1f);
   }
   
   children.Remove(toPrune);
  }
 }

 public List GetChildren()
 {
  return children;
 }

 public void PruneWholeTree()
 {
  if (children.Count > 0)
  {
   List < RayNode > ch = new List < RayNode > ();
   for (int i = 0; i < children.Count; i++)
   {
    ch.Add(children[i]);
   }

   //reverse order is important in order to prevent skipping errors
   for (int i = ch.Count-1; i > -1; i--)
   {
    PruneSubTree(i);
   }
  }
 }

 public void UpdateChildren(Vector3 startPosition)
 {
  for (int i = 0; i < children.Count; i++)
  {
   children[i].gameObject.transform.position = startPosition;
   children[i].rayLR.SetPosition(0, startPosition);
  }
 }

 public void HandleMetaBall()
 {
  metaball.transform.position = (rayLR.GetPosition(0) + rayLR.GetPosition(1)) / 2f;
  Vector3 dir = rayLR.GetPosition(1) - rayLR.GetPosition(0);
  metaball.transform.rotation = Quaternion.AngleAxis(
Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg,
 Vector3.forward);
  float scaleFactor = 0;
  
  //set scaling factor in case of low distances
  if (Vector3.Distance(rayLR.GetPosition(1), rayLR.GetPosition(0)) < 5f)
  {
   scaleFactor = 0.2f;
  }

  metaball.transform.localScale = new Vector3((scaleFactor + 1.1f) * 
Vector3.Distance(rayLR.GetPosition(1), rayLR.GetPosition(0)), 2f, 1f);
 }
}


This simple fix solved several other problems as well. The targets need to keep track of what light is hitting them, and previously each light ray would send a message letting the target know this. I would have to clear the list at the beginning of a light update cycle, then add each light ray, and at the end of a frame, I would have to perform a check to see if the target's condition had been met. The exact timing of that check is important, because performing the check in-between light rays being added would lead to false positives: the target needs to be hit by red light and only red light, for example, and after one check it is being hit by red light, so the condition is marked as being satisfied. But then orange light and yellow light send their messages to the target and now the condition is not satisfied. But the message has already been sent to the game manager, which now congratulates the player on solving the puzzle even though they have not solved the puzzle. You get the idea. Since I was performing these checks every time an object was moved, that just increased the chance that one poorly timed message would screw the whole thing up. Now, I only perform these checks when a light ray first hits the target and when it stops hitting the target, greatly reducing the chance of a false positive.

One final problem that has only come to light recently is null references. Normally, these would be huge signal fires that something has broken somewhere, but these only started showing up when I changed some of my laser code to be updated just as the program stops or shuts down. When I did that, suddenly the console was getting clogged by null reference errors. Luckily, the fix was really simple: inside the PruneSubTree function, I now check if the child to delete is null and if it is not, then I delete it. That's it: no more errors.

I've been spending so much time going through and fixing these problems, updating the chargeable objects and activate-able objects, etc, that I still have not gotten around to finishing the code for my heat-able objects. But I hope to have that finished before New Years.

No comments:

Post a Comment