ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Parrying Sword #29 : [기획][프로그래밍] 물 구현 및 최적화
    유니티 게임 개발/메인 프로젝트: Parrying Sowrd 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();
            }  
        }
    }

     

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

Designed by Tistory.