beat saber like game(hit detection)(i also dont know what 'hit detection' falls under so its here)

I’m trying to figure out something wrong with my beat saber clone’s hit detection, and I know its an issue with hit detection because in the normal game I can play the same level just fine, and get the SS rank(keep that in mind).(it accepts official AND modded beatmaps as input)

for reference, the map I’m using is {Beat saber vol1: beat saber[expert]}, so I shouldn’t be missing half of the notes.

the problem is that it counts some hits as misses, and what’s annoying me is that it only happens sometimes. I just cant nail it down to a specific root cause. often with dot notes, sometimes with grouped ones, and sometimes completely at random.

here is the method I use to move and detect hits on the notes

the sabers just track the velocity of the tip for swing direction and have capsule colliders slightly larger than the radius of the blade, and kinematic rb’s.

the notes themselves look to a conductor for the song’s time and are spawned by the conductor(which reads the beatmap for the spawn times) and has a target time to get sliced so there’s no position incrementing and it is 100% synced. they have a trigger for collision and one larger for sound, each, and i use vector3.angle on 2 flattened vector3’s to eliminate depth being a slice factor.

the conductor handles the spawning, getting time and scoring the player on their hits(or lack thereof).

since i cant figure out if something is causing something else, ive tried all i can think of, here is the code, in order of [conductor, note, saber].
CONDUCTOR

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

public class Conductor : MonoBehaviour
{
    public float time, speed;
    public float bpm, jumpDistance;
    public TextAsset dat;
    public AudioSource song;
    public AnimationCurve noteJumpCurve, noteRotationTransformationCurve, noteWobbleCurve;
    public GameObject noteObject;
    NoteEventData[] notes;
    LightEventData[] light;
    public float width, height;
    float[] lightLerp;
    public Material[] lights;
    SongData songData;
    public TextMeshPro hits, miss;
    int notePlacementStartCap = 0, lightEventCap = 0;
    int cutNotes = 0, misses = 0, beat = 0;
    Color[] lightColors;
    void Start()
    {
        lightColors = new Color[lights.Length];
        for (int i = 0; i < lights.Length; i++)
        {
            lightColors[i] = lights[i].color;
            lights[i].color = (Color.clear);
        }
        time = 0;
        string jsonMapData = Beat2Yeet.Program.YeetFromBeat(dat.text, bpm);
        GUIUtility.systemCopyBuffer = jsonMapData;
        songData = MapParser.FromMapJson(jsonMapData);
        notes = songData.notes;
        light = songData.lights;
        lightLerp = new float[lights.Length];
    }

    // Update is called once per frame
    void Update()
    {
        for (int i = 0; i < lights.Length; i++)
        {
            lights[i].color = (Color.Lerp(lights[i].color, Color.clear, lightLerp[i] - 0.01f));
            lightLerp[i] += Time.deltaTime / 15;
        }
        time = song.time;
        for (int i = notePlacementStartCap; i < notes.Length; i++)
        {
            if (((notes[i].time / bpm) * 30) < time + 5)
            {
                Debug.Log($"spawning a note with a time target of {((notes[i].time / bpm) * 60 / 6)} beat measures");
                notePlacementStartCap++;
                Note note = Instantiate(noteObject).GetComponent<Note>();
                note.targetTime = ((notes[i].time / bpm) * 30);
                note.conductor = this;
                float x = (notes[i].x-1.5f) * width;
                float y = 0.7f + (notes[i].y * height);
                if (notes[i].x == 0 || notes[i].x == 3)
                    note.edge = true;
                else
                    note.edge = false;
                note.groundPos = 0.6f;
                note.gridPos = new Vector2(x, y);
                note.type = notes[i].type;
                note.rotation = notes[i].rotation;
            }
        }
        for (int i = lightEventCap; i < light.Length; i++)
        {
            if (((light[i].time / bpm) * 30) < time)
            {
                lightEventCap++;
                switch (light[i].type)
                {
                    case 2:
                        lights[1].color = lightColors[1];
                        lightLerp[1] = 0;
                        break;
                    case 3:
                        lights[0].color = lightColors[0];
                        lightLerp[0] = 0;
                        break;
                    case 4:
                        lights[2].color = lightColors[2];
                        lightLerp[2] = 0;
                        break;
                    case 8:
                        Rotate(2);
                        break;

                }
            }
        }
    }
    public void CutNote()
    {
        for (int i = 0; i < 2; i++)
        {
            int x = Random.Range(0, lights.Length);
            lights[x].color = lightColors[x];
        }
        cutNotes++;
        hits.text = cutNotes.ToString();
    }
    public void MissNote()
    {
        //for (int i = 0; i < lights.Length; i++)
        //{
        //    lights[i].color = Color.red / 3;
        //}
        misses++;
        miss.text = misses.ToString();
    }
    public int RingsRotate = 0;
    public void Rotate(int i)
    {
        RingsRotate += i;
    }
    private void OnApplicationQuit()
    {
        for (int i = 0; i < lights.Length; i++)
        {
            lights[i].color = lightColors[i];
        }
    }
}

NOTE

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using EzySlice;
public class Note : MonoBehaviour
{
    public float targetTime;
    public Conductor conductor;
    public float rotation;
    float positionAlongTrack;
    public float groundPos;
    float timeUntilHit;
    Transform targetLookAt;
    public Vector2 gridPos;
    bool miss = false;
    public Material left, right;
    public NoteType type;
    Vector3 eulerWobble;
    public ParticleSystem shockwave;
    bool jumped = false;
    public GameObject arrow, dot, bomb;
    public bool edge;
    void Start()
    {
        targetLookAt = GameObject.FindGameObjectWithTag("MainCamera").transform;
        eulerWobble = Random.onUnitSphere;
        if (type == NoteType.right || type == NoteType.rightUniversal)
            GetComponent<Renderer>().material = right;
        else if (type == NoteType.left || type == NoteType.leftUniversal)
            GetComponent<Renderer>().material = left;
        if(type == NoteType.bomb)
        {
            GetComponent<MeshRenderer>().enabled = false;
            bomb.SetActive(true);
        }
        else if(type == NoteType.rightUniversal)
        {
            dot.SetActive(true);
        }
        else if (type == NoteType.leftUniversal)
        {
            dot.SetActive(true);
        }
        else if (type == NoteType.right)
        {
            arrow.SetActive(true);
        }
        else if (type == NoteType.left)
        {
            arrow.SetActive(true);
        }
    }
    bool hasBeenSliced = false;
    bool wasSliceCorrect = false;
    // Update is called once per frame
    void Update()
    {
        timeUntilHit = targetTime - conductor.time;
        positionAlongTrack = (((timeUntilHit * conductor.speed * conductor.noteJumpCurve.Evaluate(timeUntilHit * conductor.speed)) / 15) * conductor.jumpDistance) + 1.7f;
        //if ((!jumped) && (conductor.noteJumpCurve.Evaluate(positionAlongTrack) < 1.1));
        //{
        //    shockwave.Play();
        //    jumped = true;
        //    Debug.Log("Finished jumping, heading to player...");
        //}
           
       
        transform.eulerAngles = Vector3.forward * Mathf.LerpAngle(0, rotation, conductor.noteRotationTransformationCurve.Evaluate(timeUntilHit * conductor.speed)) + (eulerWobble * conductor.noteWobbleCurve.Evaluate(timeUntilHit * conductor.speed));
        transform.eulerAngles = new Vector3(transform.eulerAngles.x, Quaternion.LookRotation(targetLookAt.position - transform.position).y, transform.eulerAngles.z);
        if (positionAlongTrack > 0)
        {
            transform.position = (Vector3.forward * positionAlongTrack) + new Vector3(gridPos.x, Mathf.Lerp(groundPos, gridPos.y, conductor.noteRotationTransformationCurve.Evaluate(timeUntilHit * conductor.speed)), 0);
            if (edge)
                transform.position -= Vector3.forward / 8;
        }
        else
        {
            transform.position = Vector3.Lerp(transform.position,(Vector3.forward * timeUntilHit * 35) + (Vector3)gridPos, Time.deltaTime * 10);
            if ((!miss) && type != NoteType.bomb)
            {
                if(!hasBeenSliced && !wasSliceCorrect)
                    conductor.MissNote();
                if (hasBeenSliced)
                {
                    if (wasSliceCorrect)
                        conductor.CutNote();
                    else
                        conductor.MissNote();
                    Destroy(gameObject);
                }

            }
               
            if (positionAlongTrack < -100)
                Destroy(gameObject);
            miss = true;
        }
    }
    private void OnTriggerEnter(Collider other)
    {
        bool ignoreDir = (type == NoteType.leftUniversal || type == NoteType.rightUniversal);
        if (type == NoteType.right)
        {
            if (other.CompareTag("rightSaber"))
            {
                if(ignoreDir)
                    wasSliceCorrect = true;
                else if(type != NoteType.rightUniversal)
                {
                    Saber saber = other.GetComponentInParent<Saber>();
                    if ((Vector3.Angle(-saber.movementVector, transform.up) < 60f) && saber.movementVector.magnitude > 0.2f)
                        wasSliceCorrect = true;
                    else
                        wasSliceCorrect = false;
                }
                else
                {
                    wasSliceCorrect = true;
                }

            }
            else if(other.CompareTag("leftSaber"))
            {
                wasSliceCorrect = false;
            }
            if(type == NoteType.bomb)
            {
                wasSliceCorrect = false;
            }
        }
           
        else
        {
            if (other.CompareTag("leftSaber"))
            {
                if (ignoreDir)
                    wasSliceCorrect = true;
                else if (type != NoteType.leftUniversal)
                {
                    Saber saber = other.GetComponentInParent<Saber>();
                    if ((Vector3.Angle(-saber.movementVector, transform.up) < 60f) && saber.movementVector.magnitude > 0.2f)
                        wasSliceCorrect = true;
                    else
                        if(!wasSliceCorrect){wasSliceCorrect = false;}
                }
                else
                {
                    wasSliceCorrect = true;
                }
            }
            else if (other.CompareTag("rightSaber"))
            {
                wasSliceCorrect = false;
            }
            if (type == NoteType.bomb)
            {
                wasSliceCorrect = false;
            }

        }
        if (other.CompareTag("rightSaber") || other.CompareTag("leftSaber"))
        {
           
            Saber s = other.GetComponentInParent<Saber>();
            Debug.Log(Vector3.Dot(-s.movementVector, transform.up));
            GameObject[] halves;
            if (type == NoteType.right)
                halves = gameObject.SliceInstantiate(s.transform.position + s.transform.up, Rotate(s.movementVector, Mathf.Deg2Rad * 90), right);
            else
                halves = gameObject.SliceInstantiate(s.transform.position + s.transform.up, Rotate(s.movementVector, Mathf.Deg2Rad * 90), left);
            Rigidbody r0 = halves[0].AddComponent<Rigidbody>();
            Rigidbody r1 = halves[1].AddComponent<Rigidbody>();
            r0.AddForce((Vector3)((Rotate(s.movementVector, Mathf.Deg2Rad * 90) * 500) + Vector2.up * 50 + s.movementVector * -5 * s.speed * 5));
            r1.AddForce((Vector3)((Rotate(s.movementVector, Mathf.Deg2Rad * -90) * 500) + Vector2.up * 50 + s.movementVector * -5 * s.speed * 5));
            r0.AddTorque(Random.onUnitSphere * 800);
            r1.AddTorque(Random.onUnitSphere * 800);
            DeleteOnCondition d0 = halves[0].AddComponent<DeleteOnCondition>();
            DeleteOnCondition d1 = halves[1].AddComponent<DeleteOnCondition>();
            d0.killCondition = KillCondition.PositionYLess;
            d0.value = -10;
            d1.killCondition = KillCondition.PositionYLess;
            d1.value = -10;
            hasBeenSliced = true;
            dot.SetActive(false);
            arrow.SetActive(false);
            bomb.SetActive(false);
            GetComponent<Renderer>().enabled = false;
        }
    }
    public static Vector2 Rotate(Vector2 v, float delta)
    {
        return new Vector2(
            v.x * Mathf.Cos(delta) - v.y * Mathf.Sin(delta),
            v.x * Mathf.Sin(delta) + v.y * Mathf.Cos(delta)
        );
    }
}

SABER

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Saber : MonoBehaviour
{
    public float speed;
    public Vector2 movementVector;
    Vector3 oldup = Vector3.forward;
    void Update()
    {
        movementVector = (Vector2)(transform.up - oldup).normalized;
        speed = (transform.up - oldup).magnitude * 2;
        oldup = transform.up;
    }
}

This is probably only going to be tracked down by writing logging output to a text file and playing the game until the bug manifests.

When you see the bug, exit the game and go study the log file to see why the inputs (timings, whatever) were declared a miss when you think they should be a hit.

You simply must find a way to get the information you need in order to reason about what the problem is.

What is often happening in these cases is one of the following:

  • the code you think is executing is not actually executing at all
  • the code is executing far EARLIER or LATER than you think
  • the code is executing far LESS OFTEN than you think
  • the code is executing far MORE OFTEN than you think
  • the code is executing on another GameObject than you think it is

To help gain more insight into your problem, I recommend liberally sprinkling Debug.Log() statements through your code to display information in realtime.

Doing this should help you answer these types of questions:

  • is this code even running? which parts are running? how often does it run? what order does it run in?
  • what are the values of the variables involved? Are they initialized? Are the values reasonable?
  • are you meeting ALL the requirements to receive callbacks such as triggers / colliders (review the documentation)

Knowing this information will help you reason about the behavior you are seeing.

You can also put in Debug.Break() to pause the Editor when certain interesting pieces of code run, and then study the scene

You could also just display various important quantities in UI Text elements to watch them change as you play the game.

If you are running a mobile device you can also view the console output. Google for how on your particular mobile target.

Here’s an example of putting in a laser-focused Debug.Log() and how that can save you a TON of time wallowing around speculating what might be going wrong: