다양한 기록

Parrying Sword #29 : [기획][프로그래밍] 물 구현 및 최적화 본문

유니티 엔진/Parrying Sowrd

Parrying Sword #29 : [기획][프로그래밍] 물 구현 및 최적화

라구넹 2023. 6. 21. 14:12

// 물을 구현하게 된 사유 및 일기

최근 몬스터 헌터 3G를 하였다.

그 게임에 수중전이라는 개념이 나오는데, 물 속에서 전투를 하는 컨셉이다.

많은 비판을 받는 요소이긴 하지만, 그래도 나름 감성이 있어 마음에 들었기에 물을 구현을 해보려고 했다.

 

그리고 마땅히 마음에 드는 방법이 안 떠올라서 진행이 막혔다.

사실 화산 지대도 만들다 말았기에 먼저 화산 지대를 하면 됐지만,

물 구현이 자꾸 마음에 걸려 코딩 테스트 연습 문제를 풀거나 하면서 다른 걸 했다. 결국 거의 한 달을 진행을 안했다.

 

그러던 중, 고등학교 때의 친구에게 전화가 왔다. 뭘 하고 있느냐 물었는데, 하필 게임 중이라 게임 중이라고 답을 했더니

만든다던 게임은 관둔 거냐고 물어봐서 슬슬 다시 할 때가 되어, 다시 진행하였다.

/////////

 

처음에는 지형을 폭탄으로 터트리면 물이 떨어지는 등, 물 덩어리를 여러개 만들고 겹쳐놔서

다른 지형 지물과 상호 작용이 가능하도록 만들고 싶었습니다.

 

가능은 할 것 같았는데, 아무리 생각해도 퀄리티가 높아지면 높아질수록

그만큼 많은 물 덩어리를 작게, 그리고 많이 만들어야 하니 리소스를 너무 크게 잡아 먹을 것이 분명하였습니다.

 

여기서 결국 타협하고, Sprite Shape Controller를 사용하여 물을 구현하기로 했습니다. 

Sprite Shape Controller로 물 사이에 많은 점들을 만들고, 물결이 흔들리는 것을 각 지점을 이용해서 구현하는 계획입니다.

 

물 안에서의 움직임은 컬라이더 내부에 있으면 속도와 점프력을 낮추면 됩니다.

 

첫번째 결과입니다. 물결을 Sin 함수로 계산했습니다.

일단 물체를 만들고, 그걸 쭉 길게 만들었습니다. 문제는 프레임이 너무 요동칩니다.

 

for 루프에서 분기예측 하다가 문제가 생긴 건지 아니면 물 움직임을 스프라이트로 표현하다가 문제가 생긴 건지는 모르지만,

해당 부분을 고쳐야 함은 분명합니다.

 

 

 

두번째 결과입니다. 아직 최적화가 적용된 버전은 아니고,

가만히 있을 때 잔물결이 있으면 더 예쁠까 싶어 적용시켜본 것입니다.

별로 예쁘지도 않고, 프레임이 요동치는 수준이 아니라 그냥 심하게 떨어집니다.

 

 위 영상에는 그래도 프레임이 괜찮아 보일지 몰라도, 좀 더 길어지면 10, 20프레임까지 떨어져서 도저히 안됩니다.

 

 

세번째입니다. 최적화를 했습니다.

물이 가로로 길어지면 하나의 오브젝트를 길게 늘려 for 문이 탐색해야 할 지점들의 숫자가 너무 많아지는 것이 문제라고 파악했습니다.

그래서, 물 오브젝트를 여러 개 만들고 옆에다 가져다 놔서 하나인 것처럼 눈속임을 하기로 했습니다.

 

위의 두 영상에 나온 것보다 더 길게 물을 만들었는데도 더 높은 프레임을 보여줍니다.

 

실제로는 10개를 이어 붙인 모습입니다.

이전 방식 대로는 하나의 for 문에서 100번의 루프를 하니 분기예측이 중간에 어긋나면 크게 문제가 생길 것이지만,

이렇게 10개로 나눠버리면 하나의 물 오브젝트 당 10개의 부담만 하면 됩니다.

 

세로로는 그냥 길게 만들어도 문제 없습니다.

 

플레이어가 빠진 곳을 기준으로 근처만 조금 물이 흔들리도록 코드를 짰지만,

그럼에도 하나의 오브젝트로 처리하면 흔들리지 않는 곳이 많아도 영향을 받을 수 밖에 없습니다.

이렇게 오브젝트를 나눠버려 for 문의 최대 크기를 줄이면 나머지 오브젝트는 멀쩡히 분기예측이 진행이 될 것이라 파악됩니다.

 

물론 분기예측 문제가 아닐 수도 있으나,

해당 부분이 문제일 것이라 파악하고 이를 위한 해결 방안을 적용하여 크게 개선되었습니다.

 

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

public class LiquidController : MonoBehaviour
{
    private SpriteShapeController spriteShapeController;
    private Spline spline;
    private GameObject player;
    private PlayerController playerCtrl;
    private Rigidbody2D playerRB;
    private BoxCollider2D col;
    private float orgMovingWeight;
    private float orgGravity;
    private float width, height;
    private int waterLineCount;
    private float[] wave;
    private int length;
    private float waveConstant = 50.0f;
    private int interval = 1;

    public bool onWaterLine = false;
    public float waveWegiht = 1.5f;
    public float density = 1.65f;
    public GameObject heightObj, widthObj;

    private void Awake()
    {
        col = GetComponent<BoxCollider2D>();
        player = GameObject.Find("Player");
        playerCtrl = player.GetComponent<PlayerController>();
        playerRB = player.GetComponent<Rigidbody2D>();
        spriteShapeController = GetComponent<SpriteShapeController>();
        spline = spriteShapeController.spline;
        orgMovingWeight = playerCtrl.movingWeight;
        orgGravity = playerRB.gravityScale;

        width = widthObj.transform.position.x - transform.position.x;
        height = heightObj.transform.position.y - transform.position.y;

        waterLineCount = 0;

        spline.Clear();

        Vector3 pos = new Vector3(0, height, 0);
        spline.InsertPointAt(0, pos);

        // 1번 ~ waterLineCount 까지 중간 지점
        for( int i = 1; i * interval < width; i++ )
        {
            waterLineCount++;
            pos = new Vector3(i * interval, height, 0);
            spline.InsertPointAt(i, pos);
        }

        pos = new Vector3(width, height, 0);
        spline.InsertPointAt(waterLineCount + 1, pos);

        pos = new Vector3(width, 0, 0);
        spline.InsertPointAt(waterLineCount + 2, pos);

        pos = new Vector3(0, 0, 0);
        spline.InsertPointAt(waterLineCount + 3, pos);

        length = waterLineCount + 1;

        col.offset = new Vector2(width / 2.0f, height / 2.0f);
        col.size = new Vector2(width, height);

        wave = new float[waterLineCount + 2];
        length = waterLineCount + 2;

        for( int i = 0; i < length; i++ )
        {
            wave[i] = 0.0f;
        }

        spline.SetHeight(0, 0.1f);
        spline.SetHeight(waterLineCount + 2, 0.1f);
    }


    private void Update()
    {
        if (!onWaterLine) return;

        for( int i = 0; i < length - 1; i++ )
        {
            Vector3 pos = new Vector3(i * interval, height + wave[i] * Mathf.Sin(i * waveConstant / 150), 0.0f);

            spline.SetPosition(i, pos);

            wave[i] = wave[i] / 1.03f;
            if( wave[i] < 0.15f )
            {
                //wave[i] = 0.149f;
                wave[i] = 0.0f;
            }
            
        }

        if( waveConstant < 360.0f || waveConstant > 0.0f)
        {
            waveConstant = (waveConstant + 1.4f * interval);
        }
        else
        {
            waveConstant = (waveConstant - 1.4f * interval);
        }
        
    }
    

    private void waveUpdate(int index, float weight, int direction)  // direction = 0 > 시작 -1 왼쪽 1 오른쪽
    {
        if (index < 0 || index >= length) return;
        if (weight < 0.1f) return;

        wave[index] = weight;
        if ( direction != 1 )
        {
            waveUpdate(index - 1,  weight / ( 2.0f * interval ), -1);
        }

        if( direction != -1 )
        {
            waveUpdate(index + 1,  weight / ( 2.0f * interval ) , 1);
        }
    }

    private void makeWave()
    {
        int index = (int)(player.transform.position.x - transform.position.x) / interval;

        if (index < 0 || index >= length) return;

        waveUpdate(index, waveWegiht, 0);
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.tag != "PlayerBody") return;


        if( onWaterLine & !playerCtrl.grounded )
        {
            makeWave();
        }
    }

    private void OnTriggerStay2D(Collider2D collision)
    {
        if (collision.tag != "PlayerBody") return;

        playerRB.velocity = new Vector2(playerRB.velocity.x, Mathf.Clamp(playerRB.velocity.y, -20.0f, 40.0f));

        playerCtrl.movingWeight = orgMovingWeight / density;
        playerRB.gravityScale = orgGravity / 5.0f;
        playerCtrl.weakJump(true);
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.tag != "PlayerBody") return;
        

        playerCtrl.movingWeight = orgMovingWeight;
        playerRB.gravityScale = orgGravity;
        playerCtrl.weakJump(false);

        if (onWaterLine && player.transform.position.y > transform.position.y + height * transform.localScale.y)
        {
            makeWave();
        }  
    }
}

 

코드가 좀 지저분하여 나중에 수정할 가능성이 크지만, 일단 올립니다.