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;
}
}