턴제제작

근접 전투

아이고이아 2019. 2. 16. 06:19

https://store.steampowered.com/app/2114090/Goetita_Turnbased_City/

 

Goetita: Turn-based City on Steam

Goetita: Turn-based city is a turn-based strategy game controlling the element of luck through rational judgment. Survive even in a desperate situation when all your resources are running out with your strategy in the cold and dark city!

store.steampowered.com

 

 근접 전투 시스템이 이 게임에 알파고 오메가라서 근가 단기간에 쉽게 만들어지지 않는 거 같다. 한 달째 전투 시스템에 매달리고 있으니 말이다. 일단 알고리즘 자체는 플레이어를 선택했을 때 일단 에너미 리스트를 검색해서 에너미 타일 옆에 이동 가능한 영역이 있는지 확인을 한다. 이동 가능한 영역이 있으면 아이콘을 활성화를 한다. 아이콘의 경우엔 일단 색을 달리 하는 걸로 하고 일단 기능 구현이 끝이 나면 잉크 스케이프로 만들어야겠다. 그다음에 타일에 이동 가능한 영역을 표시를 해준다. 노란색이나 주황색으로 하면 될 듯싶다. 클릭을 하면 플레이어가 이동을 하고 모션을 재생한다. 모션을 제생 하면서 채력또한 바꾸어 준다. 모션이 끝나면 플레이어의 행동력을 낮추고 선택 불가능하게 한다. 이 부분은 저번에 만들어 놓은 함수를 재사용하면 될 듯싶다. 

 갈라테이아를 만들기 시작한 시점은 블렌더 고퀄리티 캐릭터 만들기 부터니까 3월 3일이라고 보면 될 듯싶다. 내달이면 1년이 되는 날이다. 앞으로 1년 정도 더 하면 되지 않을까 싶다. 일단 킥스타터 모금을 할 수 있을 정도까지 달리도록 하자. 아마 인벤토리랑 인공지능 플레이어와 에너미 캐릭터 의복 디자인까지 만들면 가능하지 않을까 싶다. 적절한 타이밍을 잡도록 하자. 

  일단 플레이어를 클릭했을때 제어하는 곳이 어디인지 알아봐야겠다. 

    public void SelectPlayer (Player thisPlayer)
    {
        if (turnState != Turn.PlayerTurn) return;

        foreach (var player in activePlayerList)
        {
            if (player.playerTurnState == PlayerTurnState.Active)
            {
                player.playerTurnState = PlayerTurnState.Waiting;
                board.ResetBoard();
                player.activeOff.Invoke();      
            }
        }

        currentPlayer = thisPlayer;
        currentPlayer.playerTurnState = PlayerTurnState.Active;
        currentPlayer.activeOn.Invoke();
        board.ResetBoard();
    }

 

 여기 있구나.

    public void CheckCloseCombat()
    {
        var checkRange = 2f;

        foreach (var enemy in enemyList)
        {   
            foreach (var direction in GameUtility.eightDirections)
            {
                RaycastHit hit;
                Physics.Raycast (enemy.transform.position, direction, out hit, checkRange);
                print (hit.transform.GetComponent<TileNode>().tileStyle);
                if (hit.transform.GetComponent<TileNode>().tileStyle == TileStyle.OneArea || hit.transform.GetComponent<TileNode>().tileStyle == TileStyle.TwoArea)
                {
                    print ("Ready");
                }
            }
        }
    }

 

 일단 좌표를 얻어냈다. 다음 할 일은 이들을 배열에 담고 어떤 어떤 타일이 근접 공격이 가능한 타일인지 표시를 해주는 걸 거다.

https://icon-icons.com/

 아이콘을 무료로 다운로드할 수 있는 사이트가 있다. 일을 덜은 듯싶다. 

https://game-icons.net/

 원인데 도로 베는 걸로 하면 될듯싶다. 일단 고민이 되는 게 달려가서 때리는데 주변에 에너미가 하나만 있는 경우에는 그 녀석을 때리면 되는데 둘 이상 있는 경우에는 어떻게 해야 하는 거다. 이때는 바로 발동되는 게 아니라 어느 적을 공격할지 한 번 더 물어봐 줘야 하는 듯싶다. 아니면 검을 클릭을 하고 적을 클릭을 해줘야 어디로 이동할지 뜨도록 하는 거다. 잠깐 엑스컴이 어떤 방식을 선택했는지 보고 와야겠다. 

 해당 노드에 마우스를 가져다 두면 그 에너미 기준으로 테두리가 생기고 화살표가 생기는 듯 싶다. 만약 둘 이상이 있는 경우 일단 한 명만 뜨되 마우스가 다른 한 명의 오브젝트 위로 올려진다면 그 에너미 기준으로 바뀌고 또 다른 에너미를 마우스 오버하지 않는 이상 아까 그 노드에 가면 새로운 오브젝트를 기준으로 테두리가 뜨게 해야겠다. 이경우 에너미를 저장하는 걸 만들고 타일에 올려놨을 때 null일 때는 시계방향 같은 걸로 넣고 눌이 아닐 때는 저장된 에너미를 기준으로 뜨도록 해야겠다. 저장된 에너미가 바뀌는 경우는 겹치는 타일의 경우에는 다른 에너미에 마우스를 올렸을 때고 겹치지 않는 다른 타일로 넘어가는 경우로 하면 되겠다. 고것 참 생각할게 한두 가지가 아닌 듯싶다. 만들기는 만들어 놓지만 가급적 캐릭터 인공지능이 2칸 이상 거리를 번리도록 해야겠다. 그래야 근접 공격할 때 불편함도 없고 포탄에 다 쓸려나가지 않을 거니 말이다. 그리고 아이콘이 마우스를 올리면 떠야 되는 듯싶다. 화살표 아이콘도 필요할 듯싶다. 

 이게 코딩이 내 적성인 줄 알았는데 이건 그냥 할 줄 아는 거고 원래 재능은 3D 쪽 인 듯싶다. 이게 어릴 쩍부터 종이 접기나 종이 모형 만드는 걸 많이 해서 그런 듯싶다. 

 아 그리고 플레이어 옆에 에너미가 있어도 검 공격이 가능하도록 떠야 한다. 이 경우에 해당하는 경우 그냥 에너미를 클릭하는 경우에 검 공격이 발동되도록 해야겠다. 일단 이게 가장 만들기 편할 듯싶으니까 이것부터 만들어야겠다. 

 검공 격을 위 방식이 아니라 타일을 표시해 주고 에너미가 1명인지 확인을 하고 중복된 경우에는 적 캐릭터 위에! 를 뛰워서 선택을 하도록 하는 것도 고려 사항이다. 아니면 표시를 해주고 맘에 안 들면 캐릭터 마우스 오버를 바꾸면 돌려가는 첫 번째 방법도 있을 듯싶다. 

 커런트 타일이라는 거를 만들고 에너미 타일이나 이동 불가능 타일의 경우에는 넣지 않는다면 이전 거쳐왔던 타일이 무엇이었는지 알 수 있을 듯싶다. 이 경우 어느 에너미를 선택할지의 문제를 쉽게 해결할 수 있을 듯싶다. 타일이 아닌 에너미를 클릭을 해야 공격이 발동되는데 이전에 거쳐왔던 타일이 이동 타일이 되는 거다. 물론 타일을 클릭해도 마찬가지로 에너미를 저장해 놓았기 때문에 저장된 에너미를 공격하도록 해야겠다. 발동이 되면 null로 만들어 주는 걸 잊으면 안 될 듯싶다. 아 그리고 화면 돌리는 거 아이콘으로 추가하고 Tap키나 아이콘으로 다음 플레이어로 넘어가는 것도 구현해야 한다. 이경우에 리스트 0과 n-1을 연결하는 것이 필요할 듯싶다. 여하튼 쉽지가 않은 듯싶다. 영상을 보니까 이게 적이 뜨면 적 아이콘을 클릭하면 자동으로 디폴트로 설정한 공격이 뜨도록 되어 있는 듯싶다. 타일이 근접 공격이 가능한 타일인지 근접 공격이 불가능한 타일인지에 대한 이넘또한 만들어 놓아야 할 듯싶다. 

 아 그리고 UI 배치를 바꿔야 할 거 같다. 윈도나 최신 우분투의 장점을 고려해서 만들어야 할 듯싶다. 일단 메뉴바가 따로 있었는데 프로필을 클릭하면 정보라든지 이런게 뜨도록 해야 할 듯싶다. 마치 윈도 키를 누르면 되듯이 말이다. 그리고 액티브 스킬과 패시브 스킬 정보창을 그것 기준으로 순차적으로 배치를 해야 만들기가 편할 듯싶다. 중앙 아래에 만들려고 하니까 이것도 만들려면 좀 복잡한 알고리즘이 필요할 듯싶어서다. 이경우 총 몇 개까지 등록을 할 수 있는가 확인을 해야 할 듯싶다. 

음... 먼가 이상하다...

 처음 게 낳은 게 괜스레 바꾼 듯싶다.

 이 부분은 그냥 이걸로 가야겠다.

 선택 플레이어를 바꾸는 거는 일단 시간 날 때 만들도록 하고 이야기 나온 김에 카메라 돌리는 건 버튼을 활성화해야겠다.

    public void RotateButon (int clock)
    {
        int newClockIndex = currentClock.index + clock;
        if (newClockIndex < 0) newClockIndex = 3;
        else if (newClockIndex > 3) newClockIndex = 0;
        currentClock = clockwiseList[newClockIndex];
        this.transform.rotation = currentClock.clockwiseRotation;
    }

 

 유니티 버튼 시스템은 void만 가능한 거 같다. 테스트에 보니까 아이콘 순서가 저렇게 되어야 하는 이유가 있더라. 

 아글고 스트리밍인가 거기 넣어두면 암호화가 안 되는 거 같으니까 스크립트나 리소스 같은 건 다 거기다 몰아넣고 모드 만들고 싶으면 마음껏 만들도록 만들어야겠다. 

 전투에 들고 갈 수 있는 스킬을 제한해야 할 듯싶다. 패시브는 3개까지만 가능하도록 하고 액티브도 기본 공격을 제하고 5개 정도만 추가할 수 있도록 해야 할 듯싶다. 총이 주가 아니기 때문에 재장전의 개념을 없애면 오른쪽 아래 공간이 빈다. 여길 아이템으로 채워 넣어야 하는지 고민이다. 아마 그렇게 된다면 드론의 스킬이 주가 될 듯싶다. 

 드론을 업그레이드하면 해당 슬록이 하나씩 해금되도록 해야겠다. 이렇게 하면 윈 쪽 상단이 비는데 여기다가는 미션 정보 같은걸 넣으면 될 듯싶다.  

 근접 전투 코딩을 해야 하는데 UI 손보느라 정신없다. 원래는 근접 공격이 가능한 경우 아이콘을 활성화하는 걸 만들려고 했다. 근데 굳이 활성화할 필요는 없고 그냥 아무것도 안 보여 주면 될 듯싶다. 

 거의 웬만한 스킬이라든지 이런 부담을 덜은 듯싶다. 사이트에 광고 팍팍 눌러주고 왔다. 왠지 오픈소스를 너무 많이 써서 나중에 크레디트 만들 때 만만치 않을 듯싶다. 나중에 따로 필요한 것이 있으면 지금 다운로드한 스타일의 작품들을 참조해서 일관적인 느낌이 나도록 잉크 스케이프로 만들면 될 듯싶다. 아니면 살짝 수정하거나 짜깁기를 하고 말이다. 지금도 몇 개는 조금 수정한 거다.

 아이콘 크기가 일정한 게 더 보기 좋은 거 같긴 하다. 시야도 더 넓어지고 말이다. 이렇게 하면 스킬 개수의 제한이 덜 빡세 진다. 나중에 턴당 횟수 제한이 있는 것과 쿨타임이 있는 것도 구연해야 겟다. 아 그리고 패시브 스킬의 경우에는 드론을 더 세지게 할 것인가 아니면 플레이어를 더 세지게 할 것인가 결정을 해야 하기 때문에 가장 중요한 선택 요소가 될 듯싶다. 

 아이콘을 보면서 오히려 스킬을 어떤 걸 만들어야 할지 아이디어가 떠오르기도 한다. 파이어 폭스, VS코드, 블렌더, 리눅스 같은 특별 스킬도 만들어야 하지 않을까 싶다. 

 나중에 드론이 자연스럽게 따라오게 하려면 보간을 사용해야 할 듯싶다. 이 경우에는 Lerp함수를 쓰면 된다. 

 아 그리고 최근에 베이킹파우더를 사서 사타구니랑 겨드랑이랑 귀밑에 바르고 몇 분 있다가 씻었는데 신기하게 냄새가 다 사라져 버렸다. 원래 옷이 썩어서 중화시키려고 2Kg 자리를 샀다. 검색해 보니 데오드란트 대신 사용한다는 사람도 있어서 발라봤는데 말이다. 

 일단 버튼을 누르면 공격 가능 영역이 뜨는 거부터 만들어야겠다. 

 방향이 8개가 다 안 뜬다 멀 잘못한 걸까.

 엉뚱한 데에 겹쳐서 생성이 되는 문제였다. 타일 노드의 콜라이더 크기를 줄이면 되지 않을까 싶다. 

 역시나 가로세로 콜라이더가 대각선 콜라이더로 레이 케스트를 보내는 것을 막아서다. 이게 콜라이더는 조금씩 타일과 타일 사이에 빈틈을 만들어 줘야 하구나. 만들어 보지 않고서는 알 수 없는 노하우 같은데 있다는 걸 깨달았다. 첫 작품 완성하면 책이나 한 권 출판해야 할 듯싶다. 

먼가 안 예쁜데... 큐브가 뜨는 게 더 나아 보인다. 

 이게 더 훨씬 나은 듯싶다. 체력바 디자인이 안예뻐서 더 좋은 방식을 찾아봐야겠다. 가로로 옆으로 세워야 하나 그런 생각이 들기도 한다. 일단 체력바에 의해서 캐릭터가 가려지는 게 가장 큰 문제다. 

 일단 채력바 문제는 좀 더 고민이 필요할 듯싶다. 이게 캐릭터 크기가 작기 때문에 생기는 문제인 듯싶다. 일단은 생성을 했으니 취소를 하는 걸 만들어야 한다. 이 넘을 활용 해서 캐릭터 선택도 취소하는 것까지 만들면 되겠다. 상태 패턴을 많이 쓰게 되니까 이 넘을 자주 사용하게 되는 듯싶다. 

 멀 잘못했는지 바운더리가 처음 선택했을 때 안 뜬다. 머가 문제인지 모르겠다. 일지를 보니까 큐브로 8개 만드는 거 사이에 사라졌는데 인지를 못하고 있었던 거 같다. 

 해당 문제가 있을 때 해결했어야 했다. 모르고 지나쳤더니 머가 문제인지 알 수가 없다. 

 원인을 알아냈다. 0.8로 줄였는데 레이 캐스트 탐지범위를 1로 해두어서 탐지를 못한 거였다.

 일지를 꼼꼼히 써놓은 게 도움이 된다. 

    void Update()
    {
        if (Input.GetMouseButtonDown (1) && currentPlayer != null)
        {
            if (currentPlayer.userState == UserState.Base)
            {
                currentPlayer.activeOff.Invoke();
                currentPlayer.userState = UserState.Non;
                currentPlayer = null;
                board.ResetBoard();
            }
        }
    }

 

 일단 이렇게 만들어 봤는데 문제는 나간 다음에 다시 클릭하는 게 안된다는 거다. 이게 다시 자기 자신을 클릭하는 건 안되고 다른 캐릭터 클릭한 다음에 자기을 클릭하면 또 그건 된다는 거다. 어떻게 해야 이문제를 해소할 수 있을까.


    void Update()
    {
        if (Input.GetMouseButtonDown (1) && currentPlayer != null)
        {
            if (currentPlayer.userState == UserState.Base)
            {
                currentPlayer.activeOff.Invoke();
                currentPlayer.userState = UserState.Non;
                currentPlayer.playerTurnState = PlayerTurnState.Waiting;
                currentPlayer = null;
                board.ResetBoard();
            }
        }
    }

 

 플레이어 상태도 대기로 다시 만들어 주면 해당 문제가 해결된다.  그다음에 만들게 근접 전투를 하려고 했는데 취소를 하면 베이스 상태로 다시 돌아가는 거다. 이게 유저 상태랑 플레이어 상태랑 일치시킬까도 했는데 그냥 분리하는 게 더 나은 거 같아서 분리했다. 아 그리고 두 번 클릭하면 한 번 더 생성하는 것도 막아야겠다. 

    public void ShowCloseCombat ()
    {
        if (currentPlayer.userState == UserState.CloseCombat) return;

        foreach (var node in closeCombatList)
        {
            Instantiate (utillityImage, node.transform.position, Quaternion.Euler (90f, 0, 0), closeUtillity);
        }
        currentPlayer.userState = UserState.CloseCombat;
    }

 

 이렇게 하면 되려나. 나중에는 그냥 리턴하는 게 아니라 근처에 바로 앞에 적이 있으면 한번 더 누르면 바로 그 적을 타격하는 편의 기능을 넣어야 할 듯싶다. 아니면 새 아이콘을 만들던지 말이다. 지금은 본인의 자리에서는 타격이 불가능하다고 나와있다. 일단은 클로우즈 컴벳 상태에서 다시 베이스 상태로 돌아가는 걸 만들어 봐야겠다. 

 슬로 모션 같은 건 크리티컬 히트나 막타를 먹였을 때 아니면 크리티컬로 막타를 먹였는데 그게 평타로는 한방 킬이 안 나오는 경우였을 때로 해야 하나 고민이다. 그건 차차 해결하도록 하자 아직 급한 건 아니니 말이다. 

 무언가를 바라보게 만드는 함수는 트렌스폼 LookAt이라는 함수인 듯싶다. 이것도 나중에 쓸 일이 있을 듯싶다. 보통 이런 뜬금없는 유니티 내장 함수 이야기는 쉴 때마다 유튜브 강의를 보기 때문에 나오는 거다. 기준점을 잡고 회전하게 하려면 홀더라는 빈 오브젝트를 만들면 되는 듯싶다. 물리적인 연출을 만들 때는 리지드 바디 Addforce를 쓰면 될 듯싶다. 문득 팔다리 모두 에드 포스를 받아서 흐느적거리는 거는 어떻게 만드는지 궁금해진다. 이게 언리얼에는 있는 게 확실하다. 엑스컴에 이 기능이 있으니 말이다. 이게 있으면 좀 더 타격감이 사는데 시간이 날 때 찾아봐야겠다. 아직 급한 거는 아니니 말이다. 

 ContactPoint contactPoint = other.contacts [0]을 통해서 표면을 얻고

 Quaternion.lookRotation (contactPoint.normal)을 통해서 부딪친 면을 반환하게 할 수 있는 듯싶다. 

 나중에 칼이 에너미를 피격했을 때 효과를 디테일하게 만들 때 쓸 수 있을 듯싶다. 피가 뿜어져 나오는 파티클 연출이 필요한 듯싶다. 이것도 아직은 급한 게 아니다. 물론 근접 전투 시스템 후반부에 파티클 시스템과 함께 해결해야 할 문제이다.   

 사운드 매니저에 배열을 만들고 수정하려면 클래스 위에 System.serialriseble을 하면 되는 듯싶다. 이게 최대한 스크립트에서 수정하고 싶은 쪽으로 코딩을 하고 싶은 게 스크립트를 수정해서 모드 제작을 수월하게 하기 위한 목적이 있다. 나중에 스크립트랑 리소스랑 수정 교체가 싶도록 공개 폴더로 만들 생각이다. 

 다름 게임 보니까. 체력바를 꼭 머리 위에만 둔 게 아니라 다양하게 배치했더라. 굳이 위에 둘 필요까지는 없을 듯싶다. 

    public void DestroyCloseUtilty()
    {
var closedUtilityChild = closeUtillity.GetChild (0);
DestroyImmediate (closedUtilityChild.gameObject);
Instantiate (closedUtilityObject, Vector3.zero, Quaternion.identity, closeUtillity);
closedUtilityChild = closeUtillity.GetChild (0);
    }

 

 이런 걸 만들었다. 잘 작동한다. 문제는 이런 식으로 노드를 리셋하는 코드는 다시 사용될 여지가 있다는 거다. 따라서 재사용을 할 수 있도록 바꾸는 작업이 필요하다. 

public static GameObject holder;
 
    public static void ResetObjects (Transform node)
    {
var child = node.GetChild (0);
DestroyImmediate (child.gameObject);
Instantiate (holder, Vector3.zero, Quaternion.identity, node);
    }

 

 이렇게 생겨먹은걸 만든다. 지금 스크립트가 이데아에 있어서 그런지 holder부분이 작동하지 않는다.

    public static void ResetObjects (Transform node, GameObject holder)
    {
var child = node.GetChild (0);
DestroyImmediate (child.gameObject);
Instantiate (holder, Vector3.zero, Quaternion.identity, node);

    }

 

 이렇게 해준다. 이 코드를 적용해야 할 곳이 아직 두 곳이 남아 있다. 

 정리하는 김에 굳이 Public은 아닌데 Public으로 되어 있는 부분을 수정해 주도록 하자. 

  List <TileNode> currentBaseBoundary;

List<TileNode> currentDoubleBoundary;
List<TileNode> currentObstacleBoundary;

[SerializeField] Transform boundary;
[SerializeField]Transform boundaryChild;
[SerializeField] GameObject boundaryObject;
[SerializeField] GameObject boundaryBlock;
[SerializeField]GameObject tile;
Vector3 box = new Vector3 (1f, 1f, 1f);
 
public List<TileNode> NodeList {get {return nodeList;}}
List<TileNode> nodeList;

Vector2 boardSize = new Vector2 (50f, 50f);
 
public PathFinding PathFinding {get {return pathFinding;}}
PathFinding pathFinding;
GameManager gameManager;
Shield shield;

 

 이런 식으로 public을 제한적으로 쓰고 그걸 쓸 때도 읽기 전용으로만 쓰도록 바꾸어 주었다. 

 다음으로 바운더리 차일드라는 부분을 수정해 줄 필요가 있다. 이게 일종에 지우고 다시 생성하고 이런 걸 반복하는 거다. 

  void MakeBoundary (TileNode node, string nodeLayer)

{
if (boundary.GetChild(0) == null)
{
Instantiate (gameManager.holder, Vector3.zero, Quaternion.identity, boundary);
}

foreach (var direction in GameUtility.directions)
        {
            if (Physics.Raycast (node.transform.position, direction, GameUtility.interval, LayerMask.GetMask (nodeLayer)))
            {
if (direction == Vector3.forward)
{
Instantiate (boundaryBlock, node.transform.position + direction, Quaternion.identity, boundary.GetChild (0));
}
else if (direction == Vector3.left)
{
Instantiate (boundaryBlock, node.transform.position + direction, Quaternion.Euler (0f, 90f, 0f), boundary.GetChild (0));
}
else if (direction == Vector3.back)
{
Instantiate (boundaryBlock, node.transform.position + direction, Quaternion.identity, boundary.GetChild (0));
}
else if (direction == Vector3.right)
{
Instantiate (boundaryBlock, node.transform.position +direction, Quaternion.Euler (0f, 90f, 0f), boundary.GetChild (0));
 
}
            }
}
}

 

 

 이런 식으로 눌을 대비하는 코드를 만들어 준다. 

 위계질서 같은걸 만들어 봤다. 

 대충 코드 정리를 끝냈는데 너무 많은 데를 수정에서 일일이 기록에 남길 수는 없을 듯싶다.  

https://store.steampowered.com/app/2114090/Goetita_Turnbased_City/

 

Goetita: Turn-based City on Steam

Goetita: Turn-based city is a turn-based strategy game controlling the element of luck through rational judgment. Survive even in a desperate situation when all your resources are running out with your strategy in the cold and dark city!

store.steampowered.com