BeatBreaker ๐ฅ

ํ๋ช
: ๋๋๋๋น
ํ์: ๊นํ์ฐ, ๊ฐ์น๊ธฐ
๊ฐ๋ฐ ํ๊ฒฝ: Unity 2020.1 URP, Visual Studio, GitLab
์ ์ ๊ธฐ๊ฐ: 2020.10.27 ~ 2020.11.24
YouTube: BeatBreaker Game Play
Index
ํ๋ก์ ํธ ์๊ฐ
Beat Breaker๋ ๋ ์์ค๋ ๋นํธ๋ฅผ ๊ธ๋ฌ๋ธ๋ก ์น๋ VR ๋ฆฌ๋ฌ ์ก์ ๊ฒ์์ด๋ค.
: target game
![]() | ![]() |
ํ๊ฒ์ Beat Saber์ BoxVR๋ก ์ก์๋ค. Beat Saber์ ๋ค์จ ๋ถ์๊ธฐ์ BoxVR์ ํ๊ฒฉ๊ฐ์ ์ต๋ํ ์ด๋ฆฌ๋ ค๊ณ ํ๋ค.
๊ตฌ์กฐ
Beat Breaker๋ ํฌ๊ฒ 3๊ฐ์ง ํํธ๋ก ๋๋๋ค; Main Room, BeatBreak Stage, Result.
: main state diagrams
Main Room ์์๋ ์จ๋ฒ๊ณผ ๋ ธ๋๋ฅผ ์ ํํ ์ ์๋ค. ๋ ธ๋๋ฅผ ์ ํํ๋ฉด BeatBreak Stage ํํธ์์ ์ด์ฌํ ๋นํธ๋ฅผ ์ณ์ ์ ์๋ฅผ ์ฌ๋ฆฌ๊ณ Result ์์ ๊ฒฐ๊ณผ๋ฅผ ํ์ธํ๊ณ ์ด๋ฆ๊ณผ ๊ธฐ๋ก์ ๋จ๊ธธ ์ ์๋ค.
์ญํ ๋ถ๋ด ๋ฐ ๊ฐ๋ฐ ์ผ์
: ํ ๊ตฌ์ฑ์
๊นํ์ฐ | ๊ฐ์น๊ธฐ |
---|---|
|
|
: ๊ฐ๋ฐ ์ผ์
ํ๋ก์ ํธ ๊ตฌํ
1. VR PLAYER

: interaction level
VR์์ ํ๋ ์ด์ด๋ ์ค์ฌ์ถ ์ญํ ์ ํ๋ค. ํ๋ ์ด์ด์ ์ฃผ๋ณ ํ๊ฒฝ, ์ฌ๋ฌผ ๊ฐ์ ์ธํฐ๋ ์ ๋ ๋ฒจ์ ๋ฐ๋ผ ์ฌ์ฉ์์ ๊ฒฝํ๋ ๋ฌ๋ผ์ง ๊ฒ์ด๋ค. Beat Breaker์ ํ๋ ์ด์ด๋ ํธ๋ ์ธํฐ๋ ์ ์ด ๋๋ถ๋ถ์ ์ฐจ์งํ๊ธฐ ๋๋ฌธ์ ๊ธ๋ฌ๋ธ์ ์ธํฐ๋ ์ ๋ ๋ฒจ์ ๋์ผ ํ์๊ฐ ์์๋ค. ๊ธ๋ฌ๋ธ ์ธํฐ๋ ์ ๋ ๋ฒจ์ ๋ค์๊ณผ ๊ฐ๋ค.
- ๊ธ๋ฌ๋ธ๋ ํญ์ ์ปจํธ๋กค๋ฌ๋ฅผ ๋ฐ๋ผ ๋ค๋ ์ผ ํ๋ค.
- ๊ธ๋ฌ๋ธ๋ ๋ชจ๋ ์ค๋ธ์ ํธ(์ ์ , ๋์ )์ ์ถฉ๋์ด ๊ฐ๋ฅํด์ผ ๋๋ค. (๊ธ๋ฌ๋ธ๊ฐ ํต๊ณผํ๋ ์ผ์ด ์์ด์ผ ๋๋ค)
- ๋นํธ๋ ์๋๋ฐฑ์ ์น๋ฉด ์ถฉ๋์ด ๋ ์ง์ ์์ ์๋ฆฌ๊ฐ ๋์ผ ๋๋ค.
a. XR Input
: input class diagram
Unity XR Toolkit์ ์ต๊ทผ์์์ผ New Input System์ผ๋ก ์ธํ ์คํฌ๋ฆฝํธ๊ฐ ๋ธ๋ ค ์ค์ง๋ง ๊ทธ ์ ๊น์ง๋ ์ธํ ๋งค๋์ ๋ฅผ ๋ฐ๋ก ์์ฑํด์ผ ํ๋ค. ํ๋ก์ ํธ์์ ์ฌ์ฉํ ์ธํ ๋งค๋์ ธ๋ VR with Andrew์ Scriptable Object ๋ฐฉ์์ ์ฌ์ฉํ๋ค.
์ธํ์ 3๊ฐ์ง๋ก ๋๋๋ค. XRButtonHandler๋ Boolean ๊ฐ์ ๋ฐํํ๋ Primary/Secondary Button์ ๋ฐ๊ณ XRAxisHandler๋ ํ๋์ ์ถ์ผ๋ก๋ง ๊ฐ์ด ๋ณํ๋ Trigger Button, Grip Button ์ด ์ฌ๊ธฐ์ ํด๋นํ๋ค. XRAxis2DHandler๋ ๋๊ฐ์ ์ถ์ ๊ฐ์ง Primary/Secondary Joystick์ด ๋๊ฒ ๋ค.
XRInputManager๋ ์์ 3๊ฐ์ง Handler๋ก ๋ง๋ค์ด์ง Scriptable Object๋ฅผ ๋ฐ์ Update์์ HandleState()
๋ฅผ ํตํด ๊ฐ์ ๊ณ์ ์
๋ฐ์ดํธ ํด์ค๋ค.
์ธํ ๊ฐ์ด ํ์ํ ์คํฌ๋ฆฝํธ๋ ํด๋น ์ธํ์ Scriptable Object๋ฅผ ๋ฐ์์ ์ฌ์ฉํ๋ฉด ๋๋ค.
b. Physics Hand

ํ๋ ์ด์ด์ ๊ธ๋ฌ๋ธ๋ ์ ์ ์ธ ์ฌ๋ฌผ๊ณผ๋ ์ถฉ๋์ด ์์ด์ผ ํ๊ธฐ ๋๋ฌธ์ Non-Kinematic Rigidbody ๋ฅผ ๊ฐ์ง๋ค. ์ด๋ ๊ฒ ๋๋ฉด ์ปจํธ๋กค๋ฌ์ ํฌ์ง์ ๊ณผ ๋กํ ์ด์ ํธ๋ ํน์ ํด์ผ ๋๋๋ฐ ์ด ๋ฌธ์ ๋ VR with Andrew์ ํ์ ๋ฐ์ Velocity Tracking ๋ฐฉ๋ฒ์ ์ฌ์ฉํ์๋ค.
XRPhysicsHand.cs
private void VelocityTrackPosition()
{
_rb.velocity *= velocityPredictionFactor;
var positoinDelta = targetPosition - _rb.worldCenterOfMass;
var velocity = positoinDelta / Time.unscaledDeltaTime;
if (!float.IsNaN(velocity.x))
{
_rb.velocity += velocity;
}
}
private void VelocityTrackRotation()
{
_rb.angularVelocity *= velocityPredictionFactor;
var rotationDelta = targetRotation
* Quaternion.Inverse(_rb.rotation);
float angleInDegrees; Vector3 rotationAxis;
rotationDelta.ToAngleAxis(out angleInDegrees, out rotationAxis);
if (angleInDegrees > 180)
{
angleInDegrees -= 360;
}
if (Mathf.Abs(angleInDegrees) > Mathf.Epsilon)
{
var angularVelocity = (rotationAxis * angleInDegrees
* Mathf.Deg2Rad) / Time.unscaledDeltaTime;
if (!float.IsNaN(angularVelocity.x))
{
_rb.angularVelocity += angularVelocity
* angularVelocityDamping;
}
}
}
Velocity Tracking ๋ฐฉ์์ Unity XR Toolkit์์ GrabbleObject๋ฅผ ์ก์ ๋ ์ฌ์ฉํ๋ ๋ฐฉ์์ค ํ๋๋ก ๊ธ๋ฌ๋ธ์๋ ์ ์ฉํ๊ฒ ๋๋ฉด ์ปจํธ๋กค๋ฌ์ ํฌ์ง์ ๊ณผ ๋กํ ์ด์ ๊ฐ์ ๋ฐ๋ผ๊ฐ๊ฒ ๋๋ค.
c. Persistent Scene
๋น๋ ์ธ๋ฑ์ค์ 0๋ฒ์งธ์ ์ฌ, Persistent Scene์ ํ๊ดด๋๋ฉด ์ ๋๋ ์ค๋ธ์ญํธ๋ค์ด ์กด์ฌํ๋ ์ฌ์ผ๋ก GameSystem, ScoreManager ๋ฐ VRPlayer๊ฐ ํฌํจ๋๋ค.
Persistent Scene์ ํญ์ ๋ก๋ ๋์ด ์๊ณ ๊ทธ ์์ ์๋ก์ด ์ฌ์ด ๋ก๋๋๋ ๋ฐฉ์์ด๋ค.
SceneLoader.cs
private IEnumerator LoadNew(string sceneName)
{
AsyncOperation loadAsyncOperation =
SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
while (!loadAsyncOperation.isDone)
{
float progress = Mathf.Clamp01(
loadAsyncOperation.progress / .9f);
yield return null;
}
}
์๋ก์ด ์ฌ์ ๋ก๋ํ ๋ LoadSceneMode.Additive
๋ชจ๋๋ก ๋ก๋ํ๋ฉด Persistent Scene์ด ์ ์ง๋ ์ฑ๋ก ์๋ก์ด ์ฌ์ด ๋ก๋๋๋ค.
private IEnumerator LoadScene(string sceneName)
{
_isLoading = true;
OnLoadBegin?.Invoke();
yield return _screenFader.StartFadeIn();
yield return StartCoroutine(UnloadCurrent());
yield return new WaitForSeconds(1f);
yield return StartCoroutine(LoadNew(sceneName));
yield return _screenFader.StartFadeOut();
OnLoadEnd?.Invoke();
_isLoading = false;
}
์๋ก์ด ์ฌ์ ๋ก๋ํ๋ ๊ณผ์ ์ ์์ ๊ฐ๋ค. ์ํฐ๋ธ๋ ์ฌ์ ์ธ๋ก๋ ํ๊ธฐ์ ์ ํ๋ฉด์ FadeIn ํ๋ค๊ฐ ์๋ก์ด ์ฌ์ ๋ก๋ํ๋ฉด์ FadeOut์ ํ๋ค.
ScreenFader.cs
private IEnumerator FadeIn()
{
while (_smh.shadows.value.magnitude >= 1)
{
_smh.shadows.value -= Vector4.one * _fadeSpeed * Time.deltaTime;
_smh.midtones.value -= Vector4.one * _fadeSpeed * Time.deltaTime;
_smh.highlights.value -= Vector4.one * _fadeSpeed * Time.deltaTime;
yield return null;
}
}
ScreenFader๋ PostProcess์ ShadowsMidtonesHighlights
์ ์ด์ฉํ๋ค.
2. MAIN ROOM

MainRoom์ ์จ๋ฒ์ ๊ณ ๋ฅด๊ณ ์๋๋ฐฑ์ ์ณ์ ๋ ธ๋๋ฅผ ๊ณ ๋ฅด๋ ๊ณต๊ฐ์ด๋ค.
a. BoxingBag

BoxingBag์ MainRoom์ ์ปจํธ๋กค๋ฌ ์ญํ ์ ํ๋ค. Prototype ๋ฒ์ ์๋ ๋ฒํผ์ ์ด์ฉํด ๋ ธ๋๋ฅผ ๊ณ ๋ฅด๋ ๋ฐฉ์์ด์์ง๋ง ์ฌ๋ฏธ๊ฐ ์๋ ์ธํฐ๋ ์ ์ด๋ผ BeatBreaker์ ์ปจ์ ๊ณผ๋ ๋ง๋ ์๋๋ฐฑ์ผ๋ก ๋ฐ๊ฟจ๋ค.
์กฐ์ ๋ฐฉ๋ฒ์ ๊ฐ๋จํ๋ค. BoxingBag์ ์ค๋ฅธ์ชฝ์ ์น๋ฉด ๋ ธ๋๊ฐ ์ค๋ฅธ์ชฝ์ผ๋ก ๋์ด๊ฐ๊ณ ์ผ์ชฝ์ ์น๋ฉด ์ผ์ชฝ์ผ๋ก ๋์ด๊ฐ๋ค. ๊ทธ๋ฆฌ๊ณ ๊ฐ์ด๋ฐ๋ฅผ ๊ฐํ๊ฒ ์น๋ฉด ๋ ธ๋๊ฐ ์ ํ๋๋ค.
: structure
![]() | ![]() |
BoxingBag์ Rigidbody๋ก ์ด๋ฃจ์ด์ ธ ์๋ค. ์ฒด์ธ์ Configurable Joint๋ก ์ฐ๊ฒฐ๋ผ ์๊ณ ๊ณ ์ ์ถ๋ง Kinematic์ด๋ค. BoxingBag ์ ๋ฌด๊ฒ๊ฐ์ ์ฃผ๊ธฐ ์ํด ์ฝ๊ฐ์ drag๊ฐ์ด ์๋ค. ๋, ์ณค์ ๋ ๋๋ฌด ๋ฉ๋ฆฌ ๋ ์๊ฐ๋ฉด ์ ๋๊ณ ํ๊ฒฉ๊ฐ์ ์ฃผ๊ธฐ ์ํด ๋ฐ๋ฅ์๋ Spring Joint๊ฐ BoxingBag์ ์ฐ๊ฒฐ๋์ด ์๋ค.
: flicker

BoxingBag์ ๊ธ๋ฌ๋ธ๋ก ์น๋ฉด ์ปจ์ ์ ๋ง๊ฒ ๋ค์จ ๋ผ์ดํธ๊ฐ ๊น๋นก์ธ๋ค.
BoxingBag.cs
private IEnumerator Flick(float duration)
{
float elapsedTime = 0f;
while (elapsedTime <= duration)
{
RandomAverage(_renderer, _color);
elapsedTime += Time.deltaTime;
yield return null;
}
}
private Color RandomAverage(Renderer renderer, Color color)
{
while (_smoothQueue.Count >= _smoothing)
{
_smoothSum -= _smoothQueue.Dequeue();
}
float randomValue = Random.Range(_minValue, _maxValue);
_smoothQueue.Enqueue(randomValue);
_smoothSum += randomValue;
renderer.material.SetColor("_EmissionColor", color * _smoothSum);
return color;
}
์๋ฆฌ๋ ๊ฒฐ๊ตญ ๋๋ค ์๋ฅผ ์ด์ฉํด ๊น๋นก์ด๊ฒ ํ๋ ๊ฒ์ด๋ค. ํ๊ฐ ์ ํด์ง _smoothing
์ ํฌ๊ธฐ๋งํผ ๋ค ์ฐฐ ๋๊น์ง intensity๊ฐ ์ฌ๋ผ๊ฐ๋ค๊ฐ ์ ํด์ง ์์ ๊ฐ์์ง๋ฉด ํ์์ ํ๋์ฉ ๋นผ์ intensity์ ๊ฐ์ ๋ณ๋์ ์ค๋ค.
b. LevelSelector

LevelSelector๋ BoxingBag์ Visual Output ์ด๋ค. UI ์ด์ง๋ง 3D ์ค๋ธ์ ํธ๋ฅผ ์ด์ฉํด ๋ง๋ค์๋ค. LevelSelector๋ ์จ๋ฒ์ ์์ผ์ ๋ฃ๋ ๊ฒ๋ถํฐ ์์๋๋ค.
: select album

์์ผ์ Unity XR Toolkit์ Socket์ ์ด์ฉํ๋ค.
LevelSelector.cs
/// <summary>
/// This is called when music block is attached to the socket.
/// </summary>
public void OnMusicBlockAttach(GameObject socket)
{
_isMusicBoxAttached = true;
GameObject interactor = socket.GetComponent<XRSocketInteractor>().selectTarget.gameObject;
_selectedMusicBlock = interactor.GetComponent<MusicBlock>();
_musicBlockAttach.PlaySoundAt(interactor.transform.position, 0.3f, 2f);
_firstSongIndex = _selectedMusicBlock.FirstSongIndex;
_lastSongIndex = _selectedMusicBlock.LastSongIndex;
_selectedSceneIndex = _firstSongIndex;
_selectedSceneTitle = SceneLoader.Instance.Scenes[_selectedSceneIndex];
AttachVisualize();
}
์์ผ์ ์จ๋ฒ์ด ๋ถ์ฐฉ๋๋ ์๊ฐ ํธ์ถ๋๊ณ XRSocketInteractor
๋ก ๋ถํฐ ์จ๋ฒ์ ์๋ ๋
ธ๋ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์จ๋ค. ๋
ธ๋ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ฉด ๋
ธ๋ ์ปค๋ฒ๊ฐ ๋ณด์ด๊ณ ๋
ธ๋๋ ์ฌ์๋๋ค.
: select music

UI์ ๋ํ ์ผ์ ์ด๋ฆฌ๊ธฐ ์ํด ๋ ธ๋ ์ปค๋ฒ๊ฐ ์์ผ๋ก ๋์ด๊ฐ ๋ ๋ง๋ค Lerp๋ฅผ ํตํ ์๋ฆฌ ์ด๋์ด ๋๊ณ ์ค์ผ์ผ๊ณผ ํฌ๋ช ๋๋ ๋ฐ๋๋ค.
LevelSelector.cs
private IEnumerator SmoothFade(GameObject songCover, Vector3 startScale, Vector3 targetScale, Color startColor, float startAlpha, float targetAlpha,
AnimationCurve ac, float duration, FadeMode fadeMode)
{
if (fadeMode.Equals(FadeMode.In)) songCover.SetActive(true);
float elapsedTime = 0f;
while (elapsedTime <= duration)
{
// ๋
ธ๋ ์ปค๋ฒ์ ํฌ๋ช
๋ ์กฐ์
Material material = songCover.GetComponent<Renderer>().material;
startColor.a = Mathf.Lerp(startAlpha, targetAlpha, ac.Evaluate(elapsedTime / duration));
material.color = startColor;
// ์ฌ์ง์ ํฌ๋ช
๋ ์กฐ์
Color imageColor = songCover.GetComponentInChildren<Image>().color;
imageColor.a = Mathf.Lerp(startAlpha, targetAlpha, ac.Evaluate(elapsedTime / duration));
songCover.GetComponentInChildren<Image>().color = imageColor;
// ์ค์ผ์ผ ์กฐ์
_lerpScaleValue = Vector3.Lerp(startScale, targetScale, ac.Evaluate(elapsedTime / duration));
songCover.transform.localScale = _lerpScaleValue;
elapsedTime += Time.deltaTime;
yield return null;
}
if (fadeMode.Equals(FadeMode.Out)) songCover.SetActive(false);
}
ํฌ๋ช
๋๋ฅผ ์กฐ์ ํ๊ธฐ ์ํด material์ Render Mode๋ Transparent๋ฅผ ์ฌ์ฉํ๊ณ ์ฌ์ง material์ ์ฐ์ ์์๋ฅผ ๋ ๋๊ฒ ์คฌ๋ค. ์๋ฆฌ ์ด๋ ๋ชจ์
๋ SmoothFade()
์ ๋ง์ฐฌ๊ฐ์ง ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๋ค.
: confirm selection

๋ ธ๋ ์ ํ์ BoxingBag์ ์ค์์ ์ผ์ ์๋ ์ด์์ผ๋ก ์ณ์ผ ์ ํ์ด ๋๋ค.
SandBagHitPoint.cs
private void TiggerEvent(Rigidbody hand)
{
BoxingBag.ActivateFlicker();
if (_selectedHitFunc.Equals(HitFunctions.Play))
{
_eventTriggerValue = BoxingBag.PlayTriggerValue;
// Play
if (hand.velocity.magnitude > _eventTriggerValue)
{
BoxingBag.DisconnectBoxingBag(hand);
onHitSandBag?.Invoke(_selectedHitFunc);
}
}
else
{
_eventTriggerValue = BoxingBag.HitTriggerValue;
// Previous, Next
if (hand.velocity.magnitude < 6f &&
hand.velocity.magnitude > _eventTriggerValue)
{
onHitSandBag?.Invoke(_selectedHitFunc);
}
}
}
public enum HitFunctions { Previous, Play, Next }
BoxingBag์ OnTriggerEnter ์ฝ๋ฐฑ์ ์ํด ํธ์ถ๋๋ค. ๋ฏธ๋ฆฌ ์ค์ ๋ _selectedHitFunc
์ ๋ฐ๋ผ LevelSelector์ ์ ๋ฌ๋๋ ๊ฐ์ด ๋ฌ๋ผ์ง๋ค.
BoxingBag.cs
public void DisconnectBoxingBag(Rigidbody rb)
{
_rootChain.breakForce = 1f;
_springChain.breakForce = 1f;
ForceTarget.drag = 0f;
ForceTarget.AddForceAtPosition(rb.velocity * _breakForce,
rb.position, ForceMode.Impulse);
ChainBreakSound.PlaySoundAt(_rootChain.transform.position);
}
์ฒด์ธ์ joint์ .breakForce
๊ฐ์ 1๋ก ๋ฐ๊ฟ์ ๋๊ณ ์ถฉ๋ ์ง์ ์ .AddForceAtPosition()
๋ฅผ ํ์๋ค.
: Game State Changer
Game State์ ์ด 4๊ฐ์ง์ด๋ค.
public enum GameState { Load, MapSelection, GameOn, Result }
GameStateChanger๊ฐ ํ์ฌ ์ํ๋ฅผ ๊ฒฐ์ ํ๊ณ ์ํ๋ง๋ค ํ์ํ ์ธํ ์ ์คํํ๋ค.
GameStateChanger.cs
private void OnEnable()
{
SceneManager.sceneLoaded += UpdateGameState;
}
๋ค์ 3๊ฐ์ง ์ํ; Load, MapSelection, GameOn ๋ ์๋ก์ด ์ฌ์ด ๋ก๋ ๋ ๋ UpdateGameState()
ํจ์์์ ์
๋ฐ์ดํธ๋๋ค. ๋ง์ง๋ง Result ์ํ๋ GameOn ์ํ์์ ์ ํํ ๋
ธ๋๊ฐ ๋๋๋ฉด ์๋์ผ๋ก ๋ฐ๋๋ค.
3. BEATBREAK STAGE

BeatBreak Stage๋ ๋นํธ์ ๋ง์ถฐ ๋ ์์ค๋ Beat๋ฅผ ๋ฐฉํฅ์ ๋ง๊ฒ ์น๋ฉด์ ์ด๋ํ๋ ๊ณต๊ฐ์ด๋ค.
a. Beat

Beat๋ ์คํฐ์ด๋ ๋ ์น๋ ๋ฐฉํฅ์ด ๋๋ค์ผ๋ก ์ ํด์ง๊ณ ๋ ์ธ์ ๋ฐ๋ผ ํ๋ ์ด์ด๋ฅผ ํฅํด ์์ง์ธ๋ค. Beat๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฌผ๋ฆฌ ์ธํฐ๋ ์ ์ด ๊ฐ๋ฅํ๋ค.
ํ๋ ์ด์ด๋ ์ ์๋ฅผ ์ฌ๋ฆฌ๊ธฐ ์ํด์ ์ ํด์ง ๋ฐฉํฅ์ ๋ง๊ฒ Beat๋ฅผ ์ณ์ผ ๋๊ณ ์ฝค๋ณด๋ฅผ ์ ์งํ๋ ๊ฒ์ด ์ค์ํ๋ค.
: points
![]() | ![]() |
Beat๋ฅผ ์ณค์ ๋ ์ ์๋ฅผ ์ธก์ ํ๋ ์ ์ฐจ๋ ๋ค์๊ณผ ๊ฐ๋ค.
OnCollisionEnter โ Check Speed โ Check HitPoint โ Calculate Angle
BoxingBag.cs
// ๊ฐ์ฅ ๋จผ์ ธ ์๋ ์ฒดํฌ๋ก Hit, Miss ์ธ์ง ํ๋ณํ๋ค.
if (_rb.velocity.magnitude > noteControl.TriggerVelocity)
{
EffectPool.Instance.PlayEffect(EffectType.punch, collision.contacts[0].point, -_rb.transform.forward);
_goodPunch.PlaySoundAt(collisionPoint);
_shadowPunch.PlaySoundAt(collisionPoint);
// ์ถฉ๋ ์ง์ ์ด HitPoint ๋ฒ์์ ์๋์ง ํ์ธ ํ๋ค.
if (dstFromHitPoint < noteControl.HitPointRadius)
{
// ๋ฒกํฐ์ ๋ด์ ์ ๊ตฌํด ๋ง์ง๋ง ํ์ด๋ ์ ์์ ์ ์ฉํ๋ค.
float dotValue = Vector3.Dot(-hitPoint.up, _rb.velocity.normalized);
_finalScore += score + Mathf.Clamp(score * dotValue, 0f, score);
ScoreManager.Instance.AddScore(_finalScore);
noteRB.AddForceAtPosition(punchVelocity, collisionPoint, ForceMode.Impulse);
}
else
{
_finalScore = score * Mathf.InverseLerp(0, 1, dstFromHitPoint);
ScoreManager.Instance.AddScore(_finalScore);
noteRB.AddForceAtPosition(punchVelocity * _reducePercentage, collisionPoint, ForceMode.Impulse);
}
}
else
{
_badPunch.PlaySoundAt(collisionPoint);
ScoreManager.Instance.Missed();
}
b. Fever Mode
Fever Mode๋ 10 ์ฝค๋ณด ์ด์ ์ ์งํ๋ฉด ์ง์ ํ๊ฒ ๋๋ค.

ํ์ง๋ง ์ฅ์ ๋ฌผ์ ์ถฉ๋ํ๊ฑฐ๋ Beat๋ฅผ ๋์น๊ฑฐ๋ Miss๊ฐ ๋ ๊ฒฝ์ฐ ์ฝค๋ณด๋ ๊นจ์ง๊ณ Fever Mode๋ ๋๋๊ฒ ๋๋ค.

![]() | ![]() |
c. Result Screen

Result UI๋ 3๊ฐ์ ํ๋๋ก ์ด๋ฃจ์ด์ ธ ์๋ค. ์ผ์ชฝ ํ๋์ Best Combo๋ Miss์ ๊ฐ์ ๊ฐ์ ๋ํ ์ผ์ ํ์ํ๋ค. ๊ฐ์ด๋ฐ ํ๋์ ์ ์์ ๋ ธ๋ ์ปค๋ฒ์ฌ์ง์ ํ์ํ๊ณ ์ค๋ฅธ์ชฝ ํ๋์ ํ๋ ์ดํ ๋ ธ๋์ ๋ญํฌ๊ฐ ํ์๋๋ค.
๊ธฐ๋ก์ ๋จ๊ธฐ๊ธฐ ์ํด์ ํค๋ณด๋๋ฅผ ๊ธ๋ฌ๋ธ๋ก ์น๊ณ Enter๋ฅผ ๋๋ฅด๋ฉด ๋ญํฌ ํจ๋์ ๊ธฐ๋ก์ ๋จ๊ธฐ๊ฒ ๋๋ค.
: physics button

Physics Button์ ๋ชฉ์ ์ ๋ง๊ฒ ์ฌ์ ์ ํด์ ์ฌ์ฉํ ์ ์๋๋ก ๋ง๋ค์๋ค.
PhysicsButton.cs
private void IsButtonPushed()
{
_currentDistance = Vector3.Distance(_btn.transform.position,
_pushPoint);
if (_currentDistance < _distance * 0.5f && !_isButtonPushing)
{
_isButtonPushing = true;
OnButtonDown(_btn.transform.position);
}
else if (_currentDistance > _distance * 0.6f && _isButtonPushing)
{
_isButtonPushing = false;
OnButtonUp(_btn.transform.position);
}
}
PhysicsButton์ ์ฝ์ด ๊ธฐ๋ฅ์ผ๋ก ๋ฒํผ์ด ๋๋ ธ๋์ง๋ฅผ ํ๋จํ๋ค. ._pushPoint
์ ._btn.transform.position
์ฌ์ด์ ๊ฑฐ๋ฆฌ๋ก ๋๋ ธ๋์ง ํ์ธํ๋ค.
PhysicsButton.cs
public virtual void OnButtonDown(Vector3 buttonPosition) { }
public virtual void OnButtonUp(Vector3 buttonPosition) { }
PhysicsButton์ ์์๋ฐ์ ์์์ ๋ชฉ์ ์ ๋ง๊ฒ ์ฌ์ ์ ํด์ ์ฌ์ฉํ๋ฉด ๋๋ค.
KeyboardButtons.cs
public override void OnButtonDown(Vector3 position)
{
onKeyPush?.Invoke(_selectedKey, position);
}
์ฌ์ ์๋ ํจ์๋ ๋ฒํผ์ด ๋๋ฆด ๋๋ง๋ค ํธ์ถ๋๋ค.
KeyboardKeyController.cs
private void OnEnable()
{
foreach (KeyboardButtons btn in _keyboardButtons)
{
btn.onKeyPush += Push;
}
}
private void Push(KeyboardKeys key, Vector3 pos)
{
_click.PlaySoundAt(pos);
switch (key)
{
default:
KeyInput(key.ToString());
break;
}
}
KeyboardKeyController์ Push()
ํจ์๋ .onKeyPush
์ ๊ตฌ๋
๋ผ ์๊ธฐ ๋๋ฌธ์ ๋ ํผ๋ฐ์ค๋ฅผ ๊ฐ์ง๊ณ ์๋ ๋ฒํผ๋ค์ด ๋๋ฆด ๋๋ง๋ค KeyInput()
์ ๊ธฐ๋ฅ์ ์ํํ๋ค.
: save system
๋ญํฌ๋ ๊ธฐ๋ก๋ ๋ json ํ์ผ ํ์์ผ๋ก ์ ์ฅ๋๋ค. Tuple์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ๋ ๋ ธ๋ ์ ๋ชฉ, ๋ญํฌ์ ๊ธฐ๋กํ ์ด๋ฆ, ๊ทธ๋ฆฌ๊ณ ํ๋ ์ดํ ๋ ธ๋์ ์ ์๋ฅผ ๋ฐ๊ฒ ๋๋ค.
SaveSystem.cs
public void Save(string song, string name, float score)
{
Records.Add(new Tuple<string, string, float>(song, name, score));
var rank = (from record in Records
orderby record.Item3 descending
select record)
.ToList<Tuple<string, string, float>>();
string jsonRecord = JsonConvert.SerializeObject(rank);
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(jsonRecord);
string format = Convert.ToBase64String(bytes);
File.WriteAllText(SAVE_FOLDER + "Ranking.json", format);
}
๊ธฐ๋ก์ ์ถ๊ฐ๋ ๋๋ง๋ค ์ ์์์ผ๋ก ๋ค์ ์ ๋ ฌํด์ ๊ธฐ๋ก๋๋ค.
public List<Tuple<string, string, float>> GetRankOfSong(string song)
{
List<Tuple<string, string, float>> rank = new List<Tuple<string, string, float>>();
foreach (Tuple<string, string, float> record in Records)
{
if (record.Item1.Equals(song))
{
rank.Add(record);
if (rank.Count == _rankTop) break;
}
}
return rank;
}
๋
ธ๋์ ๊ธฐ๋ก์ ๊ฐ์ ธ์ฌ ๋๋ Records
์ ๋ก๋๋ ์ ๋ณด์ ๋ฏธ๋ฆฌ ์ค์ ํ ๊ฐ์๋งํผ๋ง ๋ฐํํ๋ค.