BeatBreaker ๐ŸฅŠ

BeatBreaker Logo

ํŒ€๋ช…: ๋“œ๋ž๋”๋น—
ํŒ€์›: ๊น€ํ˜„์šฐ, ๊ฐ•์Šน๊ธฐ
๊ฐœ๋ฐœ ํ™˜๊ฒฝ: Unity 2020.1 URP, Visual Studio, GitLab
์ œ์ž‘ ๊ธฐ๊ฐ„: 2020.10.27 ~ 2020.11.24
YouTube: BeatBreaker Game Play


Index

1. ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

2. ํ”„๋กœ์ ํŠธ ๊ตฌํ˜„


ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

Beat Breaker๋Š” ๋‚ ์•„์˜ค๋Š” ๋น„ํŠธ๋ฅผ ๊ธ€๋Ÿฌ๋ธŒ๋กœ ์น˜๋Š” VR ๋ฆฌ๋“ฌ ์•ก์…˜ ๊ฒŒ์ž„์ด๋‹ค.

: target game
targetBS.png targetVRB.png
Fig 1. Target Games.

ํƒ€๊ฒŸ์€ Beat Saber์™€ BoxVR๋กœ ์žก์•˜๋‹ค. Beat Saber์˜ ๋„ค์˜จ ๋ถ„์œ„๊ธฐ์™€ BoxVR์˜ ํƒ€๊ฒฉ๊ฐ์„ ์ตœ๋Œ€ํ•œ ์‚ด๋ฆฌ๋ ค๊ณ  ํ–ˆ๋‹ค.

๊ตฌ์กฐ

Beat Breaker๋Š” ํฌ๊ฒŒ 3๊ฐ€์ง€ ํŒŒํŠธ๋กœ ๋‚˜๋‰œ๋‹ค; Main Room, BeatBreak Stage, Result.

: main state diagrams

Main Room ์—์„œ๋Š” ์•จ๋ฒ”๊ณผ ๋…ธ๋ž˜๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋‹ค. ๋…ธ๋ž˜๋ฅผ ์„ ํƒํ•˜๋ฉด BeatBreak Stage ํŒŒํŠธ์—์„œ ์—ด์‹ฌํžˆ ๋น„ํŠธ๋ฅผ ์ณ์„œ ์ ์ˆ˜๋ฅผ ์˜ฌ๋ฆฌ๊ณ  Result ์—์„œ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๊ณ  ์ด๋ฆ„๊ณผ ๊ธฐ๋ก์„ ๋‚จ๊ธธ ์ˆ˜ ์žˆ๋‹ค.

์—ญํ•  ๋ถ„๋‹ด ๋ฐ ๊ฐœ๋ฐœ ์ผ์ •

: ํŒ€ ๊ตฌ์„ฑ์›
๊น€ํ˜„์šฐ ๊ฐ•์Šน๊ธฐ
  • VR ํ”Œ๋ ˆ์ด์–ด
  • Game System
  • Interactive UI
  • Level Design
  • ๋น„ํŠธ ์Šคํฐ
  • ๋น„ํŠธ ๋ฆฌ๋“ฌ ์ƒ์„ฑ
  • ์žฅ์• ๋ฌผ ์›€์ง์ž„
: ๊ฐœ๋ฐœ ์ผ์ •

ํ”„๋กœ์ ํŠธ ๊ตฌํ˜„

1. VR PLAYER

vrplayer.png

: 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

physicshand.png
Fig 2. 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.gif

MainRoom์€ ์•จ๋ฒ”์„ ๊ณ ๋ฅด๊ณ  ์ƒŒ๋“œ๋ฐฑ์„ ์ณ์„œ ๋…ธ๋ž˜๋ฅผ ๊ณ ๋ฅด๋Š” ๊ณต๊ฐ„์ด๋‹ค.

a. BoxingBag

beatMain.png
Fig 3. Boxing Bag.

BoxingBag์€ MainRoom์˜ ์ปจํŠธ๋กค๋Ÿฌ ์—ญํ• ์„ ํ•œ๋‹ค. Prototype ๋ฒ„์ „์—๋Š” ๋ฒ„ํŠผ์„ ์ด์šฉํ•ด ๋…ธ๋ž˜๋ฅผ ๊ณ ๋ฅด๋Š” ๋ฐฉ์‹์ด์—ˆ์ง€๋งŒ ์žฌ๋ฏธ๊ฐ€ ์—†๋Š” ์ธํ„ฐ๋ ‰์…˜์ด๋ผ BeatBreaker์˜ ์ปจ์…‰๊ณผ๋„ ๋งž๋Š” ์ƒŒ๋“œ๋ฐฑ์œผ๋กœ ๋ฐ”๊ฟจ๋‹ค.

์กฐ์ž‘ ๋ฐฉ๋ฒ•์€ ๊ฐ„๋‹จํ•˜๋‹ค. BoxingBag์˜ ์˜ค๋ฅธ์ชฝ์„ ์น˜๋ฉด ๋…ธ๋ž˜๊ฐ€ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ๋„˜์–ด๊ฐ€๊ณ  ์™ผ์ชฝ์„ ์น˜๋ฉด ์™ผ์ชฝ์œผ๋กœ ๋„˜์–ด๊ฐ„๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ฐ€์šด๋ฐ๋ฅผ ๊ฐ•ํ•˜๊ฒŒ ์น˜๋ฉด ๋…ธ๋ž˜๊ฐ€ ์„ ํƒ๋œ๋‹ค.

: structure

chain.png bagspring.png
Fig 4. Boxing Bag Structure.

BoxingBag์€ Rigidbody๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ๋‹ค. ์ฒด์ธ์€ Configurable Joint๋กœ ์—ฐ๊ฒฐ๋ผ ์žˆ๊ณ  ๊ณ ์ •์ถ•๋งŒ Kinematic์ด๋‹ค. BoxingBag ์— ๋ฌด๊ฒŒ๊ฐ์„ ์ฃผ๊ธฐ ์œ„ํ•ด ์•ฝ๊ฐ„์— drag๊ฐ’์ด ์žˆ๋‹ค. ๋˜, ์ณค์„ ๋•Œ ๋„ˆ๋ฌด ๋ฉ€๋ฆฌ ๋‚ ์•„๊ฐ€๋ฉด ์•ˆ ๋˜๊ณ  ํƒ€๊ฒฉ๊ฐ์„ ์ฃผ๊ธฐ ์œ„ํ•ด ๋ฐ”๋‹ฅ์—๋Š” Spring Joint๊ฐ€ BoxingBag์— ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋‹ค.

: flicker

bagBlink.gif
Fig 5. 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.png
Fig 6. Level Selector.

LevelSelector๋Š” BoxingBag์˜ Visual Output ์ด๋‹ค. UI ์ด์ง€๋งŒ 3D ์˜ค๋ธŒ์ ํŠธ๋ฅผ ์ด์šฉํ•ด ๋งŒ๋“ค์—ˆ๋‹ค. LevelSelector๋Š” ์•จ๋ฒ”์„ ์†Œ์ผ“์— ๋„ฃ๋Š” ๊ฒƒ๋ถ€ํ„ฐ ์‹œ์ž‘๋œ๋‹ค.

: select album

album.gif
Fig 7. 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

selectMusic.gif
Fig 8. 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

musicPick.gif
Fig 9. Confirm Selected Music.

๋…ธ๋ž˜ ์„ ํƒ์€ 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

gameon.gif

BeatBreak Stage๋Š” ๋น„ํŠธ์— ๋งž์ถฐ ๋‚ ์•„์˜ค๋Š” Beat๋ฅผ ๋ฐฉํ–ฅ์— ๋งž๊ฒŒ ์น˜๋ฉด์„œ ์šด๋™ํ•˜๋Š” ๊ณต๊ฐ„์ด๋‹ค.

a. Beat

beat.png
Fig 10. Beat.

Beat๋Š” ์Šคํฐ์ด๋  ๋•Œ ์น˜๋Š” ๋ฐฉํ–ฅ์ด ๋žœ๋ค์œผ๋กœ ์ •ํ•ด์ง€๊ณ  ๋ ˆ์ธ์„ ๋”ฐ๋ผ ํ”Œ๋ ˆ์ด์–ด๋ฅผ ํ–ฅํ•ด ์›€์ง์ธ๋‹ค. Beat๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฌผ๋ฆฌ ์ธํ„ฐ๋ ‰์…˜์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

ํ”Œ๋ ˆ์ด์–ด๋Š” ์ ์ˆ˜๋ฅผ ์˜ฌ๋ฆฌ๊ธฐ ์œ„ํ•ด์„œ ์ •ํ•ด์ง„ ๋ฐฉํ–ฅ์— ๋งž๊ฒŒ Beat๋ฅผ ์ณ์•ผ ๋˜๊ณ  ์ฝค๋ณด๋ฅผ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค.

: points

beatHit.gif beatMiss.gif
Fig 11. Hit & Miss.

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 ์ฝค๋ณด ์ด์ƒ ์œ ์ง€ํ•˜๋ฉด ์ง„์ž…ํ•˜๊ฒŒ ๋œ๋‹ค.

feverEnter.gif
Fig 12. Enter Fever.

ํ•˜์ง€๋งŒ ์žฅ์• ๋ฌผ์— ์ถฉ๋Œํ•˜๊ฑฐ๋‚˜ Beat๋ฅผ ๋†“์น˜๊ฑฐ๋‚˜ Miss๊ฐ€ ๋‚  ๊ฒฝ์šฐ ์ฝค๋ณด๋Š” ๊นจ์ง€๊ณ  Fever Mode๋Š” ๋๋‚˜๊ฒŒ ๋œ๋‹ค.

feverEnd.gif
Fig 13. Exit Fever.
gameonlight.png gameonfever.png
Fig 14. Comparison.

c. Result Screen

result.png
Fig 15. Result UI.

Result UI๋Š” 3๊ฐœ์˜ ํŽ˜๋„๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ๋‹ค. ์™ผ์ชฝ ํŽ˜๋„์€ Best Combo๋‚˜ Miss์˜ ๊ฐœ์ˆ˜ ๊ฐ™์€ ๋””ํ…Œ์ผ์„ ํ‘œ์‹œํ•œ๋‹ค. ๊ฐ€์šด๋ฐ ํŽ˜๋„์€ ์ ์ˆ˜์™€ ๋…ธ๋ž˜ ์ปค๋ฒ„์‚ฌ์ง„์„ ํ‘œ์‹œํ•˜๊ณ  ์˜ค๋ฅธ์ชฝ ํŽ˜๋„์€ ํ”Œ๋ ˆ์ดํ•œ ๋…ธ๋ž˜์˜ ๋žญํฌ๊ฐ€ ํ‘œ์‹œ๋œ๋‹ค.

๊ธฐ๋ก์„ ๋‚จ๊ธฐ๊ธฐ ์œ„ํ•ด์„  ํ‚ค๋ณด๋“œ๋ฅผ ๊ธ€๋Ÿฌ๋ธŒ๋กœ ์น˜๊ณ  Enter๋ฅผ ๋ˆ„๋ฅด๋ฉด ๋žญํฌ ํŒจ๋„์— ๊ธฐ๋ก์„ ๋‚จ๊ธฐ๊ฒŒ ๋œ๋‹ค.

: physics button

keyboard.gif
Fig 16. Physics Keyboard.

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์— ๋กœ๋“œ๋œ ์ •๋ณด์— ๋ฏธ๋ฆฌ ์„ค์ •ํ•œ ๊ฐœ์ˆ˜๋งŒํผ๋งŒ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

updated_at 19-02-2021