게임에서 사용되는 FPS(Frame Per Second)란 무엇인지 알아보고, 유니티 엔진에서 FPS를 측정하는 방법과 게임의 목표 FPS를 설정하는 방법을 알아봅시다.

 

타임라인

0:03 FPS 개요

3:29 에디터에서 게임 뷰의 FPS 보기

4:11 FPS 출력 기능 구현하기

5:44 FPS 출력 기능 테스트

6:31 게임의 목표 프레임 설정하기

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

2D 게임에서 배경의 속도가 다르게 움직이면서 원근감 효과를 주는 기술인 패럴랙스 스크롤링을 구현하는 방법을 알아봅시다.

 

타임라인

0:41 어?

0:49 깃허브 터진 화면

1:05 리소스 임포트

1:11 스프라이트 배치

1:28 원근감 효과 코드 작성

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

게임 중간에 로딩 화면 없이 연속적으로 씬을 불러오는 스트리밍 레벨 기능을 구현하는 방법을 알아봅시다!

 

캐릭터 조작 기능이 들어있는 패키지 : https://github.com/wwer20001/wer-prog...

씬 모양을 미리 만들어둔 패키지 : https://github.com/wwer20001/wer-prog...

 

타임라인

0:03 스트리밍 레벨 기능 개요

0:40 리소스 임포트와 필요한 오브젝트 가져오기

1:28 씬 구조 살펴보기

2:32 스트리밍 씬 로드 트리거 만들기

3:02 스트리밍 씬 로드 기능 구현하기

4:30 트리거 세팅과 테스트

4:55 지나간 씬 언로드 기능 구현하기

6:09 지나간 씬이 언로드되고 캐릭터가 사라지는 문제 해결하기

7:05 다음 씬으로 완전히 넘어갔을 때 화면이 어두워지는 문제 해결하기

7:25 지나온 씬으로 다시 돌아가기 테스트

7:28 지나온 씬으로 돌아갈 때 캐릭터가 2개가 되는 문제 해결하기

8:01 스트리밍 레벨 기능 사용시 고려 사항1

8:38 스트리밍 레벨 기능 사용시 고려 사항2

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

유니티 엔진에서 차나 탈 것에 타고 내리는 기능을 만들어 봅시다!

 

차 모델 및 애니메이션 : https://drive.google.com/file/d/1Y-Ru93kIVXqjzD9xw4xPUs7VYotTFVny/view?usp=sharing 박스맨 모델 및 애니메이션 : https://drive.google.com/file/d/1EUdtP7LeL5yfyRPmKG6WGiLCXBMVkmeC/view?usp=sharing

 

타임라인

0:22 리소스 임포트

0:33 비히클 시스템 설계

5:22 설계대로 클래스 생성

7:17 캐릭터 조작 기능 구현

9:36 차량 조작 기능 구현

12:00 차에 타기 기능 구현

14:56 차에서 내리기 기능 구현

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

마우스 입력 처리를 응용해서

1. 캐릭터가 마우스 위치를 쳐다보게 하는 기능

2. 클릭으로 투사체 발사 기능

3. 휠으로 줌 인/줌 아웃하는 기능

을 구현하는 방법을 알아봅시다.

 

응용 이전에 마우스 입력 처리에 대한 기본 방법은 링크를 통해서 배우실 수 있습니다.

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

Programming 

모바일 가상 조이스틱 구현하기

 

작성 기준 버전 :: 2019.2 - 2019.4

 

[이 포스트의 내용은 유튜브 영상으로도 시청하실 수 있습니다]

 

 

 

요즘에는 오락실이 별로 많지 않지만 제가 어렸을 때 오락실에서 게임할 때는 오락실 기계에 붙어있는 조이스틱과 버튼을 사용해서 게임을 플레이 했었다. 그리고 본격적으로 PC게임으로 넘어왔을 때는 키보드와 마우스를 통해 게임 속의 캐릭터나 유닛들을 조작했다.

 

 

또 구세대 2G 폰에서는 핸드폰에 달린 숫자 버튼들이 게임을 조작하는 데 쓰였었다. 한참 모바일 기기에서 새로운 시도를 하던 때에는 게임을 위한 전용 휴대폰도 나왔었다.

 

 

하지만 어느 새 스마트폰의 시대가 와버렸는데 스마트폰은 보통 입력을 받아들일 수 있는 부분이 화면의 터치 밖에 없다. 그래서 게임 조작 방식에 많은 제약이 있을 수 밖에 없었다.

 

터치만 가능한 모바일 기기에서 어떻게 하면 좀 더 콘솔 게임과 같은 조작이 가능할까 하며 고안된게 바로 가상 조이스틱이다. 가상 조이스틱은 화면에 조이스틱 UI를 띄우고 그것을 터치하여 조작하는 방식의 컨트롤러다.

 

조이스틱 UI 배치하기

 

 

 

가상 조이스틱을 구현하기 위해서 먼저 필요한 리소스는 조이스틱을 위에서 아래로 쳐다보았을 때의 조이스틱 바닥 부분과 레버 머리부분을 표현한 이미지이다.

 

 

 

이미지 리소스를 준비하고 나면 이미지 게임 오브젝트를 두 개 만들어서 위의 이미지와 같이 넣어준다. 빨간색 레버가 자식 게임 오브젝트가 되도록 만든다.

 

 

그리고 조이스틱 레버 이미지의 위치를 화면 좌측하단에서부터 계산하기 위해서 Joystick 게임 오브젝트의 앵커를 left bottom으로 설정한다.

 

조이스틱 스크립트 작성

 

드래그 이벤트 가져오기

 

using UnityEngine;
using UnityEngine.EventSystems; // 키보드, 마우스, 터치를 이벤트로 오브젝트에 보낼 수 있는 기능을 지원

public class VirtualJoystick : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{    
    public void OnBeginDrag(PointerEventData eventData)
    {
        Debug.Log("Begin");
    }
    
    // 오브젝트를 클릭해서 드래그 하는 도중에 들어오는 이벤트
    // 하지만 클릭을 유지한 상태로 마우스를 멈추면 이벤트가 들어오지 않음    
    public void OnDrag(PointerEventData eventData)
    {
        Debug.Log("Drag");
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        Debug.Log("End");
    }
}

 

이미지 배치가 끝나면 조이스틱 기능을 만들기 위해서 VirtualJoystick이라는 이름으로 C# 스크립트를 생성한다. 그리고 제일 상단에 EventSystems 네임스페이스를 using 선언해준다. 이 이벤트 시스템 네임스페이스에는 유니티에서 키보드, 마우스, 터치 등의 사용자 입력을 오브젝트에 이벤트로 보낼 수 있는 기능들을 지원한다.

 

그 다음에는 MonoBehaviour 상속 뒤에 IBeginDragHandler, IDragHandler, IEndDragHandler를 추가한다. 여기서 I로 시작하는 이 핸들러들은 인터페이스라는 것으로 클래스와는 다르게 구현해야할 함수만을 지정해주는 것이다. 이 인터페이스를 상속받으면 인터페이스가 가지고 있는 함수들을 강제로 구현하도록 에러가 표시된다.

 

여기서 [F12]를 눌러서 일일이 구현해야할 함수를 보고 작성할 수도 있지만, 에러가 표시되고 있는 인터페이스 위에 커서를 두고 [Ctrl + .] 단축키를 누르면 구현해야할 함수들을 자동으로 생성되게 할 수 있다.

 

참고로 BeginDrag는 드래그를 시작할 때, Drag는 드래그 중일 때, EndDrag는 드래그를 끝냈을 때를 의미한다.

 

먼저 드래그를 체크해보기 위해서 각 드래그 이벤트 함수에 로그 코드를 추가하고 코드를 저장한 뒤 에디터로 돌아간다.

 

 

그리고 생성한 컴포넌트를 Joystic 게임 오브젝트에 부착하고 게임을 플레이시킨다.

 

 

그 다음 조이스틱 이미지 위에 클릭하고 드래그하면 Begin, Drag, End 순서로 로그가 출력된다.

 

레버 이미지가 드래그를 따라서 움직이게 만들기

 

using UnityEngine;
using UnityEngine.EventSystems; // 키보드, 마우스, 터치를 이벤트로 오브젝트에 보낼 수 있는 기능을 지원

public class VirtualJoystick : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    [SerializeField]
    private RectTransform lever;    // 추가
    private RectTransform rectTransform;    // 추가

    private void Awake()    // 추가
    {
        rectTransform = GetComponent<RectTransform>();
    }
    
    public void OnBeginDrag(PointerEventData eventData)
    {  
        // Debug.Log("Begin");

        // 추가
        var inputDir = eventData.position - rectTransform.anchoredPosition;
        lever.anchoredPosition = inputDir;
    }
    
    // 오브젝트를 클릭해서 드래그 하는 도중에 들어오는 이벤트    // 하지만 클릭을 유지한 상태로 마우스를 멈추면 이벤트가 들어오지 않음    
    public void OnDrag(PointerEventData eventData)
    {
        // Debug.Log("Drag");
        
        // 추가
        var inputDir = eventData.position - rectTransform.anchoredPosition;
        lever.anchoredPosition = inputDir;
    }
    
    public void OnEndDrag(PointerEventData eventData)
    {
        // Debug.Log("End");
        
        // 추가
        lever.anchoredPosition = Vector2.zero;
    }
}

 

드래그 이벤트가 제대로 동작하는 것을 확인했으니 터치한 위치를 따라서 레버 이미지가 따라 움직이도록 만들 차례이다.

 

우선 빨간 레버 이미지의 Rect Transform 컴포넌트를 가질 lever 변수와 Joystick의 Rect Transform을 가지고 있을 rectTransform 변수를 선언한다. 그리고 lever 렉트 트랜스폼은 에디터의 인스펙터 뷰에서 넣어주기 위해서 SerializeField 어트리뷰트를 걸어주고 본체의 rectTransform은 Awake 함수에서 GetComponent로 가지고 와서 저장해둔다.

 

그 다음은 터치한 위치를 찾아내서 lever 이미지가 그 위치로 이동하게 만들어 줘야한다.

 

터치한 위치는 이벤트 함수의 매개변수로 받는 eventData.position을 통해서 가져올 수 있다. 이렇게 가져온 eventData.position에서 가상 조이스틱 게임 오브젝트의 위치인 rectTransform.anchoredPosition을 빼주면 lever가 있어야할 위치를 구할 수 있게 된다. 구한 inputDir를 lever.anchoredPosition에 넣어준다. 이 과정은 OnBeginDrag와 OnDrag, 두 이벤트에서 동일하게 처리해준다.

 

그리고 마지막으로 가상 조이스틱에서 손을 뗐을 때의 이벤트인 OnEndDrag에서는 lever.anchoredPosition을 Vector2.zero로 만들어서 조이스틱의 중심으로 다시 돌아가게 만들어준다.

 

 

코드를 저장하고 에디터로 돌아간 다음에는 컴포넌트의 비어있는 lever 프로퍼티에 레버 이미지를 할당해준다.

 

 

게임을 플레이시키고 가상 조이스틱 위에 드래그해보면 빨간색 레버 이미지가 터치한 위치를 잘 따라오는 것을 볼 수 있다. 그리고 터치를 끝내면 자동으로 중심으로 레버가 돌아가기까지 한다.

 

 

하지만 너무 잘 따라와서 문제인데 드래그를 유지한 채로 조이스틱 영역 밖으로 움직이면 빨간 레버 역시 조이스틱 위치를 벗어나 버린다. 레버가 조이스틱 영역을 벗어나지 않도록 수정할 필요가 있어보인다.

 

레버 이동 제한하기

using UnityEngine;
using UnityEngine.EventSystems; // 키보드, 마우스, 터치를 이벤트로 오브젝트에 보낼 수 있는 기능을 지원

public class VirtualJoystick : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    [SerializeField]
    private RectTransform lever;
    private RectTransform rectTransform;
    // 추가
    [SerializeField, Range(10f, 150f)]
    private float leverRange;    
    
    private void Awake()
    {
        rectTransform = GetComponent<RectTransform>();
    }
    
    public void OnBeginDrag(PointerEventData eventData)
    {
        var inputDir = eventData.position - rectTransform.anchoredPosition;
        //추가
        var clampedDir = inputDir.magnitude < leverRange ? 
            inputDir : inputDir.normalized * leverRange;

        // lever.anchoredPosition = inputDir;
        lever.anchoredPosition = clampedDir;    // 변경

    }
    
    // 오브젝트를 클릭해서 드래그 하는 도중에 들어오는 이벤트
    // 하지만 클릭을 유지한 상태로 마우스를 멈추면 이벤트가 들어오지 않음    
    public void OnDrag(PointerEventData eventData)
    {
        var inputDir = eventData.position - rectTransform.anchoredPosition;
        // 추가
        var clampedDir = inputDir.magnitude < leverRange ? inputDir : inputDir.normalized * leverRange;

        // lever.anchoredPosition = inputDir;
        lever.anchoredPosition = clampedDir;    // 변경
    }
    
    public void OnEndDrag(PointerEventData eventData)
    {
        lever.anchoredPosition = Vector2.zero;
    }
}

 

먼저 레버가 움직일 수 있는 거리를 조이스틱의 중심부로부터 일정 거리로 제한시키기 위해서 float 타입으로 leverRange라는 변수를 추가한다. private으로 두되 에디터에서 수정할 수 있도록 SerializeField 어트리뷰트와 특정 범위 내에서만 값을 조절할 수 있도록 Range 어트리뷰트를 추가해준다. Range의 범위는 10에서 150 사이로 하자.

 

leverRange 변수를 만들고 나면 이벤트 함수에서 inputPos를 lever의 anchoredPosition에 바로 넣지 말고 inputPos의 길이와 leverRange를 비교한 뒤, leverRange보다 짧으면 inputPos를 바로 주고, 길면 inputPos를 정규화한 다음 leverRange를 곱하는 방식으로 inputPos의 거리를 제한하여 lever의 anchoredPosition에 넣어준다. 이 과정도 OnDrag에서 똑같이 처리해준다.

 

 

 

코드를 저장하고 에디터로 돌아가서 레버 이미지가 조이스틱 영역을 크게 벗어나지 않는 적절한 영역을 확인한다. 지금은 100 정도가 적당해 보인다.

 

 

VirtualJoystick 컴포넌트가 부착되어 있는 Joystick 게임 오브젝트를 선택하고 인스펙터 뷰에서 Lever Range 프로퍼티를 100으로 설정한다.

 

 

게임을 플레이시키고 가상 조이스틱을 움직여보면 드래그를 조이스틱 밖으로 끌어도 조이스틱의 레버가 조이스틱 영역을 벗어나지 않는 것을 볼 수 있다.

 

조작 기능 추가하기

using UnityEngine;
using UnityEngine.EventSystems; // 키보드, 마우스, 터치를 이벤트로 오브젝트에 보낼 수 있는 기능을 지원

public class VirtualJoystick : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    [SerializeField]
    private RectTransform lever;
    private RectTransform rectTransform;
    [SerializeField, Range(10f, 150f)]
    private float leverRange;
    
    private Vector2 inputVector;    // 추가
    private bool isInput;    // 추가

    private void Awake()
    {
        rectTransform = GetComponent<RectTransform>();
    }
    
    public void OnBeginDrag(PointerEventData eventData)
    {
        // var inputDir = eventData.position - rectTransform.anchoredPosition;
        // var clampedDir = inputDir.magnitude < leverRange ? inputDir 
        //     : inputDir.normalized * leverRange;
        // lever.anchoredPosition = clampedDir;

        ControlJoystickLever(eventData);  // 추가
        isInput = true;    // 추가
    }
        
    // 오브젝트를 클릭해서 드래그 하는 도중에 들어오는 이벤트
    // 하지만 클릭을 유지한 상태로 마우스를 멈추면 이벤트가 들어오지 않음    
    public void OnDrag(PointerEventData eventData)
    {
        // var inputDir = eventData.position - rectTransform.anchoredPosition;
        // var clampedDir = inputDir.magnitude < leverRange ? inputDir 
        //     : inputDir.normalized * leverRange;
        // lever.anchoredPosition = clampedDir;
        
        ControlJoystickLever(eventData);    // 추가
        isInput = false;    // 추가
    }
    
    // 추가
    public void ControlJoystickLever(PointerEventData eventData)
    {
        var inputDir = eventData.position - rectTransform.anchoredPosition;
        var clampedDir = inputDir.magnitude < leverRange ? inputDir 
            : inputDir.normalized * leverRange;
        lever.anchoredPosition = clampedDir;
        inputVector = clampedDir / leverRange;
    }
    
    public void OnEndDrag(PointerEventData eventData)
    {
        lever.anchoredPosition = Vector2.zero;
    }
}

 

이제 조작 기능을 추가할 차례이다.

 

이번에는 Vector2로 inputVector 변수와 bool 타입으로 isInput 변수를 추가한다.

 

그리고 OnBeginDrag와 OnDrag 이벤트 함수에서 inputVector 값을 넣어줄 차례인데, 두 이벤트 모두 동작하고 있는 함수의 내용이 완전히 동일하다는 것을 알 수 있다. 그렇기 때문에 ControlJoystickLever라는 함수를 새로 만들고 두 함수의 내용을 복사해서 붙여넣어 준다.

 

그 다음에는 ControlJoystickLever 함수에서 clampedDir를 leverRange로 나누어서 inputVector에 넣어준다. clampedDir를 바로 써도 될 것 같은데 굳이 leverRange로 나누어서 사용하는 이유는 이 clampedDir는 해상도를 기반으로 만들어진 값이라 캐릭터의 이동속도로 쓰기에는 너무 큰 값을 가지고 있기 때문이다.

 

이런 값으로 캐릭터를 움직이면 너무 빠른 속도로 캐릭터가 움직일게 분명하기 때문에 입력 방향의 값을 0-1 사이 값으로 만들어서 정규화된 값으로 캐릭터에 전달하기 위한 것이다.

 

그리고 화면 해상도를 기준으로 값이 정해지기 때문에 해상도가 바뀌면 입력 방향 값의 크기가 바뀌어서 캐릭터의 이동 속도가 바뀌는 문제도 있어서 0-1사이로 정규화된 값을 사용해야 한다. 캐릭터는 이렇게 전달받은 정규화된 이동 벡터에 이동속도를 곱해서 일정한 속도로 이동하게 된다.

 

그 다음엔 OnBeginDrag에서 isInput을 true로 바꿔주고 OnEndDrag에서는 false로 바꿔준다.

 

private void InputControlVector()
{
    Debug.Log(inputDirection.x + " / " + inputDirection.y);
    // 캐릭터에게 입력벡터를 전달
}

 

그 다음에는 구해낸 입력 벡터를 캐릭터에 전달하기 위한 함수로 InputContorlVector를 만든다. 이 함수 안에 캐릭터에게 이동 벡터를 전달하는 기능을 작성하면 된다. 우선 지금은 매개변수로 받은 inputVector의 값을 로그로 출력하는 코드만 작성해두자.

 

void Update()
{
    if(isInput)
    {
        InputControlVector();
    }
}

 

업데이트 함수로 가서 isInput이 true일 때, InputControlVector 함수를 호출해주면 됩니다. 

 

여기에서 한 가지 의문을 가질 수 있다. 이 InputControlVector 함수를 OnBeginDrag와 OnDrag 이벤트 함수에서도 호출할 수 있을 텐데 왜 굳이 isInput변수를 따로 만들어서 Update 함수에서 처리하는가 하는 것이다. 

 

이 부분은 OnDrag 이벤트 함수의 특성과 관련이 있다. 앞에서도 이야기했다시피 OnDrag 이벤트 함수는 오브젝트를 클릭하여 드래그하는 도중에 호출되는 이벤트이다. 하지만 여기에 꽤나 중요한 빈틈이 있다.

 

그것은 바로 클릭해서 드래그하는 도중에 클릭을 떼지 않고 마우스를 멈추면 더 이상 이벤트가 호출되지 않는다는 것이다. 그리고 다시 마우스를 움직이기 시작하면 이벤트가 호출된자. 한 마디로 드래그 도중에 마우스 이동을 멈추고 있는 동안에는 이벤트가 들어오지 않는다는 것이다.

 

그래서 드래그 도중에 터치의 이동이나 마우스 이동이 멈췄을 때도 InputControlVector 함수를 연속적으로 호출하기 위해서 isInput이 활성화된 상태일 때 Update 함수에서 지속적으로 호출하도록 만들어 준 것이다.

 

코드를 저장하고 에디터로 돌아가서 다시 테스트해보면 입력 벡터의 길이가 1이 넘지 않는 선에서 입력 방향이 출력되는 것을 볼 수 있다.

 

캐릭터 컨트롤러와 연결하기

 

이렇게 만들어진 가상 조이스틱을 캐릭터와 연결해서 조작할 수 있도록 만들어주어야 한다.

 

// 가상 조이스틱 클래스에서 컨트롤러를 소유
public CharacterController characterController;

private void InputControlVector()
{
    if (characterController)
    {
        characterController.Move(inputDirection);
    }
}

 

간단하게 예시를 들자면 위의 코드처럼 가상 조이스틱 클래스에서 캐릭터 컨트롤러를 소유하거나 콜백을 등록시켜서 호출하는 방법이 있다.

 

 

 

위의 이미지와 같은 방식으로 두 개의 조이스틱을 배치해서 사용하고 싶을 수도 있다.

 

public enum JoystickType { Move, Rotate }
public JoystickType joystickType;

private void InputControlVector()
{
    // 캐릭터에게 입력벡터를 전달
    switch(joystickType)
    {
        case JoystickType.Move:
            controller.Move(inputDirection);
            break;

        case JoystickType.Rotate:
            controller.LookAround(inputDirection);
            break;
    }
}

 

그럴 때는 가상 조이스틱에 타입을 설정할 수 있는 열거형을 만들어서 조이스틱의 타입에 따라 다르게 동작하게 만들어주면 된다.

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

Programming 

TPS 캐릭터 조작 기능 구현하기

 

작성 기준 버전 :: 2019.2

 

[이 포스트의 내용은 유튜브 영상으로도 시청하실 수 있습니다]

 

지난 포스트 중 하나에서는 마우스를 이용해서 클릭한 위치로 캐릭터를 이동시키는 RPG나 AOS 게임에서 주로 사용되는 캐릭터 조작 방법을 구현했었다.

 

이번에는 키보드로 이동하고 마우스로 원하는 대상을 바라보게 하는 TPS 게임에서 사용되는 캐릭터 조작 방법을 구현해보도록 하자.

 

리소스 임포트와 기본 애니메이션 세팅

 

본격적인 구현에 들어가기 전에 캐릭터로 사용할 모델링과 애니메이션을 영상 하단의 링크에서 다운로드 받아서 임포트한다.

 

practice-animation.zip
다운로드

 

FBX 파일을 임포트하고 나면 먼저 캐릭터의 이동 애니메이션을 보여주기 위한 애니메이션 세팅을 해보자.

 

 

먼저 임포트된 FBX 파일 중에 Run, Stand의 애니메이션 임포트 세팅 중에서 Loop Time을 체크하고 적용한다.

 

 

그리고 애니메이터 컨트롤러를 생성한 뒤 Bool 타입으로 isMove 파라미터를 추가한다. 

 

 

그 다음에는 Stand 애니메이션을 기본 스테이트로 추가하고 Run 애니메이션을 추가한 뒤 isMove의 상태에 따라서 Stand와 Run을 왔다갔다 하도록 세팅해준다.

 

캐릭터 배치와 카메라 세팅

 

 

이 작업이 끝나면 씬에 캐릭터가 돌아다닐 바닥이 될 Plane을 하나 배치하고 BoxMan@Stand를 배치하고 BoxMan에 붙어있는 Animator 컴포넌트의 비어있는 Controller 프로퍼티에 방금 만든 애니메이터 컨트롤러를 할당해준다.

 

 

애니메이션에 대한 세팅이 끝나면 카메라를 이동시켜서 TPS 게임에서 자주 보이는 카메라 구도를 만들어보자. TPS 시점의 게임에서는 이런 구도의 카메라 시점이 주로 사용된다.

 

일인칭으로 보여지는 FPS 게임은 이것과는 약간 다르게 캐릭터의 몸이 거의 보이지 않고 손 정도만 보이게 시점을 세팅하게 되지만 이후의 기능 구현 자체는 거의 비슷하게 진행하면 된다.

 

본격적인 조작 기능 구현에 들어가기에 앞서 카메라와 캐릭터 구조를 조금 더 살펴보자.

 

지금 시점 자체는 나쁘지 않지만 조작 기능이 들어가기에는 아쉬움이 많다.

 

 

특히 마우스를 상하좌우로 움직이면 카메라 역시 마우스의 움직임에 맞게 회전해야 하는데, 카메라를 잡고 위/아래/좌/우로 회전시켜보면 전혀 적절하지 못하다는 것을 깨달을 수 있을 것이다.

 

해결책 1 : 카메라를 캐릭터의 자식 게임 오브젝트로 만들기

 

 

첫 번째로 생각해볼 만한 방법은 카메라를 캐릭터의 자식 게임 오브젝트로 만드는 것이다.

 

 

카메라를 BoxMan의 자식 게임 오브젝트로 만들면 좌우로 움직이는건 정상적으로 동작한다.

 

 

하지만 위/아래로 움직여보면 캐릭터가 같이 기울어져서 적절하지 못한 것을 알 수 있다. 적절한 카메라 회전을 위해서는 캐릭터를 회전시키지 않으면서도 캐릭터를 중심으로 카메라를 회전시키는 방법을 찾아야 할 것이다.

 

해결책 2 : 카메라 암(Camera Arm) 만들기

 

 

다른 해결책으로는 카메라 암(Camera Arm) 역할을 할 빈 게임 오브젝트를 하나 만들고 BoxMan의 머리 정도의 위치로 이동시켜 주고 카메라를 이 빈 게임 오브젝트의 자식 게임 오브젝트로 만들어준다. 

 

 

이렇게 세팅해주면 상하좌우 모든 방향의 회전에도 문제없이 카메라가 캐릭터를 중심으로 움직이는 것을 확인할 수 있다.

 

 

이대로 Camera Arm을 BoxMan 밑에 배치해도 되지만 그렇게 하면 BoxMan의 회전에 Camera Arm이 묶이게 되기 때문에 Character라는 이름의 빈 게임 오브젝트를 만들어서 BoxMan과 Camera Arm을 그 아래에 배치하자. 이렇게 해주면 BoxMan의 회전과 Camera Arm의 회전이 분리되서 관리하기가 쉬울 것이다.

 

카메라 회전 기능 구현하기

 

이제 본격적으로 캐릭터 조작 기능을 만들어보자.

 

TPSCharacterController라는 이름으로 C# 스크립트를 생성하고 다음 코드를 따라 작성하자.

 

public class TPSCharacterController : MonoBehaviour
{
    [SerializeField]
    private Transform characterBody;
    [SerializeField]
    private Transform cameraArm;

    Animator animator;

    void Start()
    {
        animator = characterBody.GetComponent<Animator>();
    }
}

 

Transform 타입으로 BoxMan 모델을 관리할 characterBody 변수와 카메라의 회전을 관리할 cameraArm 변수를 선언하고 애니메이션을 관리할 Animator 변수를 선언하고 Start 함수에서 characterBody.GetComponent 함수로 BoxMan에 붙어있는 Animator 컴포넌트를 가져와서 저장해둔자.

 

private void LookAround()
{
    Vector2 mouseDelta = new Vector2(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y"));
    Vector3 camAngle = cameraArm.rotation.eulerAngles;

    cameraArm.rotation = Quaternion.Euler(camAngle.x - mouseDelta.y, camAngle.y + mouseDelta.x, camAngle.z);
}

 

먼저 마우스의 움직임에 따라서 카메라를 회전시키는 기능을 구현하기 위해서 LookAround 함수를 만든다. 

 

LookAround 함수에서는 먼저 마우스가 이전 위치에 비해서 얼마나 움직였는지 알아내야 한다. 마우스가 움직인 수치는 Input.GetAxis 함수에 "Mouse X"와 "Mouse Y"를 매개변수로 넣어서 가져올 수 있다. 

 

"Mouse X"로는 마우스를 좌우로 움직인 수치를 가져올 수 있고 "Mouse Y"로는 마우스를 위아래로 움직인 수치를 가져올 수 있다.

 

이 두 값을 Vector2로 묶어서 mouseDelta 변수에 저장한다. 참고로 프로그래밍에서 이전 값과 현재 값의 차이를 주로 delta라는 용어로 표현한다. 

 

그 다음에는 cameraArm의 rotation 값을 오일러 각으로 변환해둔다. 그리고 camAngle과 mouseDelta를 합쳐서 새로운 회전값을 만들어낸 다음 cameraArm.rotation에 넣어줘야 한다.

 

우선 마우스 좌우 움직임으로 카메라 좌우 움직임을 제어해야 하기 때문에 mouseDelta의 x 값을 camAngle의 y에 더해준다. 그리고 마우스 수직 움직임은 카메라 상하 움직임을 제어해야 하기 때문에 mouseDelta의 y 값을 camAngle의 x에 추가해준다.

 

이 마우스 수직 움직임의 경우에는 한국에서는 마우스를 위로 움직이면 위를 쳐다보고 아래로 움직이면 아래를 쳐다보게 하는 방식을 사용하는 유저들이 더 많지만 북미에서는 이 움직임을 반대로 사용하는 경우가 꽤 있다고 한다. 그렇게 때문에 별도의 옵션을 둬서 두 가지 조작 방식 중에 사용자가 원하는 방식을 선택할 수 있도록 하는게 좋다. 

 

우선 한국에서 주로 쓰는 방식으로 구현하자. 마우스 방향과 바라보는 방향을 일치시키려면 mouseDelta.y를 camAngle.x에서 빼주면 된다.

 

void Update()
{
    LookAround();
}

 

그리고 LookAround 함수를 Update 함수에 호출하게 만들어준 뒤 코드를 저장하고 에디터로 돌아가보자.

 

 

그 다음 Character 게임 오브젝트에 생성한 컨트롤러 컴포넌트를 붙이고 프로퍼티들을 할당해준다.

 

 

게임을 플레이시키고 마우스를 움직여보면 우선 카메라는 제대로 움직이는 것 같지만 카메라 상하 회전에 제한이 없어서 그런지 마우스를 움직이다보면 화면이 뒤집어지는 경우가 발생한다.

 

카메라 각도 제한하기

 

이런 문제를 막기 위해서는 카메라의 x 회전을 제한해야 한다.

 

위쪽 회전은 약 70도를 제한하고 아래쪽 회전은 약 25도를 제한하도록 하자. 다만 x 회전 값을 출력해보면 알겠지만 수평을 기준으로 위쪽으로 회전하면 0도에서 90도까지 각도가 나오고 아래쪽으로 회전하면 360도부터 270도까지 각도가 나온다.

 

private void LookAround()
{
    Vector2 mouseDelta = new Vector2(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y"));
    Vector3 camAngle = cameraArm.rotation.eulerAngles;
    
    //추가
    float x = camAngle.x - mouseDelta.y;
    if (x < 180f)
    {
        x = Mathf.Clamp(x, -1f, 70f);
    }
    else
    {
        x = Mathf.Clamp(x, 335f, 361f);
    }

    cameraArm.rotation = Quaternion.Euler(x, camAngle.y + mouseDelta.x, camAngle.z);
}

 

우선 camAngle.x - mouseDelta.y로 만들어낸 값을 바로 회전값에 넣지 말고 따로 x에 저장한다. 그리고 회전값에 넣어주기 전에 x값의 상태에 따라서 처리하는 과정을 구현해야 한다.

 

먼저 x가 180도보다 작은 경우는 위쪽으로 회전하는 경우이므로 x의 값을 -1도에서 70도까지 제한시켜둔다. 여기서 0도가 아닌 -1도로 최저점을 잡는 이유는 0도로 잡으면 카메라가 수평면 아래로 내려가지 않는 문제가 발생하기 때문이다.

 

그리고 x각이 180도보다 큰 경우는 아래쪽으로 회전하는 경우로 25도를 제한하기 위해서 335도에서 361도 사이로 값을 제한시킨다.

 

이렇게 구해진 x값을 camAngle의 x값 자리에 넣어준다.

 

 

카메라 회전을 제한시킨 기능을 에디터에서 테스트해보면 위아래 회전에 제한이 걸려서 카메라가 뒤집어지는 문제가 발생하지 않는 것을 확인할 수 있다.

 

이동기능 추가하기

 

이제 여기에 이동 기능을 추가할 차례이다.

 

이동방향 구하기

 

private void Move()
{
    Debug.DrawRay(cameraArm.position, cameraArm.forward, Color.red);
}

 

움직이는 기능을 추가하기 위해서 Move 함수를 만든다. 

 

캐릭터를 이동시키기 위해서는 먼저 캐릭터가 이동할 방향을 찾아야 한다. 이동할 방향으로 가장 적합한 대상은 카메라가 바라보고 있는 방향일 것이다.

 

cameraArm.forward를 사용하면 될 것 같은데 우선 이 값이 적당한 방향인지 확인하기 위해서 Debug.DrawRay로 카메라 암 위치에서 cameraArm.forward 방향으로 에디터에서 선으로 그려보자.

 

void Update()
{
    LookAround();
    Move();
}

 

Update 함수에서 Move 함수를 호출하게 만든다.

 

 

게임을 플레이시키고 마우스를 움직여보면 씬 뷰에서 Camera Arm 위치에 빨간색 선이 카메라 암이 쳐다보는 방향을 표시하는 것을 볼 수 있다.

 

캐릭터의 이동 방향으로 쓰기에 적당해 보이지만 위쪽이나 아래쪽을 쳐다볼 때는 선이 기울어지는 것을 볼 수 있다. 정상적인 이동 방향을 보장하기 위해서는 이 선을 수평으로 변환해야 한다.

 

private void Move()
{
    Debug.DrawRay(cameraArm.position, 
        new Vector3(cameraArm.forward.x, 0f, cameraArm.forward.z).normalized, Color.red);
}

 

새 벡터를 만들면서 cameraArm.forward의 y값을 넣지 않는 것으로 높이를 없애고 normalized 시켜서 길이를 1로 맞춰준다.

 

 

다시 에디터에서 게임을 플레이시키고 마우스를 움직여보면 위나 아래를 쳐다봐도 빨간 선의 방향이 수평을 유지하는 것을 볼 수 있다.

 

이동기능 구현하기

 

private void Move()
{
    Vector2 moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
    bool isMove = moveInput.magnitude != 0;
    animator.SetBool("isMove", isMove);
    if (isMove)
    {
        Vector3 lookForward = new Vector3(cameraArm.forward.x, 0f, cameraArm.forward.z).normalized;
        Vector3 lookRight = new Vector3(cameraArm.right.x, 0f, cameraArm.right.z).normalized;
        Vector3 moveDir = lookForward * moveInput.y + lookRight * moveInput.x;

        characterBody.forward = lookForward;
        transform.position += moveDir * Time.deltaTime * 5f;
    }
}

 

그럼 다시 스크립트 에디터로 돌아가서 이동 기능을 구현하는 것을 진행해보자.

 

우선 Input.GetAxis 함수로 "Horizontal"과 "Vertical"로 이동 입력 값을 가져와서 moveInput 벡터를 만든다. 그리고 moveInput의 길이로 입력이 발생하고 있는지 판정하여 isMove 변수에 넣어준다.

 

moveInput의 길이가 0이면 이동 입력이 없는 것이고 0이 아니면 이동 입력이 발생하고 있는 것이다.

 

이 isMove를 animator의 "isMove"에 세팅해주면 이동 입력이 발생할 때는 걷는 애니메이션을 재생하고, 이동 입력이 없을 때는 대기 애니메이션을 재생할 것이다.

 

그리고 아까 전에 구해낸 방향을 바라보는 방향으로 저장한다. 거기에 추가로 cameraArm.right 역시 정면과 마찬가지로 평면화시켜서 저장해둔다.

 

이렇게 구해낸 lookForward와 lookRight에 moveInput을 곱해서 더하면 바라보고 있는 방향을 기준으로 이동 방향을 구할 수 있다. 그리고 구해낸 이동 방향과 Time.deltaTime, 이동속도를 모두 곱해서 transform.position에 더해주면 캐릭터를 이동시킬 수 있게 된다.

 

이동시 바라보는 방향 1 : 카메라가 바라보는 방향

 

private void Move()
{
    Vector2 moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
    bool isMove = moveInput.magnitude != 0;
    animator.SetBool("isMove", isMove);
    if (isMove)
    {
        Vector3 lookForward = new Vector3(cameraArm.forward.x, 0f, cameraArm.forward.z).normalized;
        Vector3 lookRight = new Vector3(cameraArm.right.x, 0f, cameraArm.right.z).normalized;
        Vector3 moveDir = lookForward * moveInput.y + lookRight * moveInput.x;

        characterBody.forward = lookForward;
        transform.position += moveDir * Time.deltaTime * 5f;
    }
}

 

마지막으로 캐릭터가 바라보는 방향을 정하는 방법을 여러 가지로 구현할 수 있다.

 

첫 번째 방법은 캐릭터를 이동시킬 때 시선을 정면에 고정시키는 방법이다.

 

characterBody의 forward를 lookForward로 정해주면 된다.

 

 

캐릭터를 움직여보면 캐릭터를 앞뒤좌우 어느 방향으로 움직이든 카메라가 바라보는 방향을 바라본다.

 

이 기능을 위해서는 캐릭터가 좌우로 움직일 때는 옆걸음을 치는 애니메이션을 넣고, 뒤로 움직일 때는 뒷걸음을 치는 애니메이션을 넣어줘야 할 것이다.

 

이런 식으로 움직이는 방법은 캐릭터가 언제나 정면을 바라보기 때문에 좀 더 직관적인 컨트롤을 할 수 있다는 장점이 있다.

 

이동시 바라보는 방향 2 : 이동방향

 

private void Move()
{
    Vector2 moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
    bool isMove = moveInput.magnitude != 0;
    animator.SetBool("isMove", isMove);
    if (isMove)
    {
        Vector3 lookForward = new Vector3(cameraArm.forward.x, 0f, cameraArm.forward.z).normalized;
        Vector3 lookRight = new Vector3(cameraArm.right.x, 0f, cameraArm.right.z).normalized;
        Vector3 moveDir = lookForward * moveInput.y + lookRight * moveInput.x;

        characterBody.forward = moveDir;
        transform.position += moveDir * Time.deltaTime * 5f;
    }
}

 

characterBody.forward를 moveDir로 지정해주면 된다.

 

 

캐릭터를 움직여보면 캐릭터는 카메라가 바라보는 방향을 기준으로 앞뒤좌우로 움직이지만 정면을 바라보지 않고 이동해야하는 방향을 바라보는 것을 알 수 있다.

 

이렇게 구현된 컨트롤은 정면만 바라보는 컨트롤에 비해서 복잡하기는 하지만 좀 더 섬세한 방향 컨트롤이 가능해진다.

 

이 두 가지 방식 중에서 게임에서 필요한 방식을 선택해서 구현하면 된다.

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

Programming 

클릭한 위치로 캐릭터를 이동시키는 기능 구현하기

 

작성 기준 버전 :: 2019.2

 

[이 포스트의 내용은 유튜브 영상으로도 시청하실 수 있습니다]

 

게임의 조작 방식에는 굉장히 다양한 방법이 있다. 그 중에서 쿼터 뷰 형식의 RPG 게임이나 AOS 장르에서는 맵의 지형을 클릭하고 그 클릭한 위치로 캐릭터를 이동시키는 조작 방식을 사용한다.

 

쿼터 뷰 RPG나 AOS 장르처럼 클릭한 위치로 캐릭터를 이동시키는 기능을 만드는 방법을 알아보자.

 

클릭한 위치로 캐릭터 이동시키기

 

public class ClickMovement : MonoBehaviour
{
    private Camera camera;

    private bool isMove;
    private Vector3 destination;

    private void Awake()
    {
        camera = Camera.main;
    }
}

 

클릭한 위치로 캐릭터를 이동시키는 기능을 구현하기 위해서 먼저 위와 같은 변수들을 선언해준다.

 

Camera는 스크린 좌표계로 표시되는 마우스의 위치를 월드 좌표계로 표시하기 위해서 선언했다. 그리고 isMove는 캐릭터가 움직이고 있는 중인지 판정하기 위해서, destination은 캐릭터가 목표 위치를 기억하기 위해서 선언했다.

 

그리고 Awake 함수에서 씬에 존재하는 메인 카메라를 가져와서 camera 매개변수에 저장해준다.

 

private void SetDestination(Vector3 dest)
{
    destination = dest;
    isMove = true;
}

 

SetDestination 함수를 만들어서 그 안에 매개변수로 받아온 dest를 destination에 넣어주고 isMove를 true로 만들어 주는 코드를 작성한다. SetDestination 함수는 클릭으로 찾아낸 마우스의 위치를 저장해두고 이동을 시작하게 하는 역할을 한다.

 

private void Move()
{
    if (isMove)
    {
        if (Vector3.Distance(destination, transform.position) <= 0.1f)
        {
            isMove = false;
            return;
        }
        var dir = destination - transform.position;
        transform.position += dir.normalized * Time.deltaTime * 5f;
    }
}

 

Move 함수를 만든다. Move 함수에서는 isMove가 true일 때, 즉 SetDestination에서 목적지를 지정한 뒤에 목적지와 캐릭터의 거리가 0.1 유니티 미터보다 작으면 이동을 중지하고, 아직 도착하지 못한 상태라면 캐릭터의 위치에서 목적지로 향하는 방향을 구해서 캐릭터를 이동시키는 작업을 처리한다.

 

void Update()
{
    if (Input.GetMouseButton(1))
    {
        RaycastHit hit;
        if (Physics.Raycast(camera.ScreenPointToRay(Input.mousePosition), out hit))
        {
            SetDestination(hit.point);
        }
    }

    Move();
}

 

SetDestination 함수와 Move 함수를 모두 작성하면 Update 함수로 가서 마우스 오른쪽 버튼을 클릭했을 때 마우스 커서의 위치를 찾아내는 코드를 작성한다.

 

public class ClickMovement : MonoBehaviour
{
    private Camera camera;

    private bool isMove;
    private Vector3 destination;

    private void Awake()
    {
        camera = Camera.main;
    }

    void Update()
    {
        if (Input.GetMouseButton(1))
        {
            RaycastHit hit;
            if (Physics.Raycast(camera.ScreenPointToRay(Input.mousePosition), out hit))
            {
                SetDestination(hit.point);
            }
        }

        Move();
    }

    private void SetDestination(Vector3 dest)
    {
        destination = dest;
        isMove = true;
    }

    private void Move()
    {
        if (isMove)
        {
            if (Vector3.Distance(destination, transform.position) <= 0.1f)
            {
                isMove = false;
                return;
            }

            var dir = destination - transform.position;
            transform.position += dir.normalized * Time.deltaTime * 5f;
        }
    }
}

 

코드의 전문은 위와 같다.

 

 

이 컴포넌트를 캐릭터에 부착하고 콜라이더가 있는 맵에 우클릭하면 클릭한 위치로 캐릭터가 이동하는 것을 볼 수 있다.

 

기능 개선 1 : 카메라가 캐릭터 따라가게 하기

 

하지만 몇 가지 개선해야할 점이 보인다. 우선 카메라가 캐릭터를 따라가지 않아서 위쪽을 클릭하면 캐릭터가 화면 밖을 벗어나버리는 것을 볼 수 있다.

 

 

카메라가 캐릭터를 따라가게 하기 위해서 카메라를 캐릭터의 자식 오브젝트로 만들어보자.

 

 

다시 게임을 플레이시키고 우클릭으로 캐릭터를 이동시켜보면 카메라가 캐릭터를 따라서 움직이는 것을 확인할 수 있다.

 

기능 개선 2 : 캐릭터가 이동할 방향을 바라보게 하기

 

두 번째로 개선할 점은 캐릭터가 이동할 방향을 바라보게 하는 것이다. 지금은 캐릭터를 이동시켜도 게임을 시작할 때 바라보고 있던 방향만을 바라보면서 이상하게 이동하는 것을 볼 수 있다.

 

private void Move()
{
    if (isMove)
    {
        if (Vector3.Distance(destination, transform.position) <= 0.1f)
        {
            isMove = false;
            return;
        }
        var dir = destination - transform.position;
        transform.foward = dir;	// 추가
        transform.position += dir.normalized * Time.deltaTime * 5f;
    }
}

 

Move 함수를 보면 캐릭터가 이동할 방향을 구해둔 부분이 있다. 이 방향을 캐릭터의 transform.forward에 넣어주면 이동하는 방향을 바라보게 할 수 있다.

 

 

그리고 다시 게임을 플레이시키고 이동할 지점을 우클릭 해보면 우클릭한 지점을 캐릭터가 바라보면서 이동하기는 한다. 하지만 카메라가 캐릭터의 자식 오브젝트로 되어있어서 캐릭터의 방향이 바뀔 때 카메라도 함께 돌아가서 굉장히 어지럽다.

 

 

약간 구조를 바꿔야 될 것 같다. 우선 Character Root라는 이름으로 빈 게임 오브젝트를 만든다. 그리고 BoxMan을 그 아래로 옮기고 BoxMan 밑에 있는 카메라 역시 Charactor Root 아래로 옮겨준다.

 

이렇게 구조를 만들면 캐릭터를 이동시킬 때 카메라가 캐릭터가 따라서 움직이는 것을 유지하면서 BoxMan을 정상적으로 회전시킬 수 있다.

 

구조는 완성되었으니 본격적으로 기능을 수정해보자.

 

 

우선 BoxMan에 붙어있는 ClickMovement를 Character Root로 옮겨서 이동하는 주체를 Character Root로 바꿔준다.

 

[SerializeField]
private Transform character;	// 추가

private void Move()
{
    if (isMove)
    {
        if (Vector3.Distance(destination, transform.position) <= 0.1f)
        {
            isMove = false;
            return;
        }
        var dir = destination - transform.position;
        character.transform.foward = dir;	// 변경
        transform.position += dir.normalized * Time.deltaTime * 5f;
    }
}

 

캐릭터 몸체 만을 회전시키기 위해서 Transform 변수를 선언하고 회전 주체를 transform에서 character.transform으로 바꿔준다.

 

 

코드를 모두 작성하면 유니티 에디터로 돌아와서 비어있는 Character 프로퍼티에 회전해야 하는 캐릭터 게임 오브젝트를 넣어준다.

 

 

그 다음 플레이시켜보면 카메라는 회전하지 않고 BoxMan이 우클릭한 방향을 바라보며 이동하는 것을 볼 수 있다.

 

이제 기능이 완성된 것처럼 보인다.

 

 

하지만 여기에 장애물이 추가되면 어떨까?

 

 

Character Root에는 장애물과 충돌할 수 있게 Capsule Collider와 Rigidbody를 추가한다. 그리고 장애물과 충돌했을 때 캐릭터가 넘어지지 않도록 Constraints의 Freeze Rotation을 전부 체크해준다.

 

 

다시 게임을 플레이시키고 캐릭터를 벽 너머로 이동시키기 위해서 벽 너머를 우클릭하면 돌아서 이동하지 못하고 벽에 계속 부딪히기만 하는 것을 볼 수 있다. 게다가 벽 위를 클릭하면 벽 위에 올라서기 위해서 점프까지 하는 것을 볼 수 있다.

 

기능 개선 3 : 길찾기 기능 추가하기

 

꽤 재밌어보이는 기능이 완성됐지만 우리의 목적은 캐릭터가 장애물을 회피해서 이동하는 것이기 때문에 다시 개선하는 작업에 들어가보자.

 

일단 Character Root에 붙어있는 Capsule Collider의 Is Trigger를 체크해준다.

 

일반적인 RPG 게임이나 AOS 게임에서는 장애물 건너편으로 이동하기 위해서 조작을 하면 캐릭터가 자동으로 이동할 수 있는 길을 찾아서 이동하는 것을 볼 수 있다.

 

캐릭터가 장애물을 돌아서 이동하기 위해서는 길찾기 기능이 필요하다. 길찾기 알고리즘을 직접 구현할 수도 있지만, 유니티 엔진에서 제공하는 내비게이션 기능을 사용하면 훨씬 간단하게 구현할 수 있다.

 

 

우선 상단 메뉴 바에서 [Window > AI > Navigation]을 선택해서 내비게이션 뷰를 연다. 그리고 장애물과 바닥을 선택하고 내비게이션 뷰에서 Navigation Static을 체크하고 내비게이션 메시를 구워준다.

 

 

그러면 캐릭터가 이동할 수 있는 영역이 파란색으로 표시된다.

 

 

그 다음에는 캐릭터를 이 내비게이션 메시 위에서 길을 찾아 움직일 수 있는 에이전트로 만들어줘야 한다. Character Root에 NavMeshAgent 컴포넌트를 붙여준다. 

 

using UnityEngine.AI;  // 추가

public class ClickMovement : MonoBehaviour
{
    private Camera camera;
    private NavMeshAgent agent;  // 추가

    private bool isMove;
    private Vector3 destination;

    private void Awake()
    {
        camera = Camera.main;
        agent = GetComponent<NavMeshAgent>();  // 추가
        agent.updateRotation = false;  // 추가
    }

    void Update()
    {
        if (Input.GetMouseButton(1))
        {
            RaycastHit hit;
            if (Physics.Raycast(camera.ScreenPointToRay(Input.mousePosition), out hit))
            {
                SetDestination(hit.point);
            }
        }

        // Move();
        LookMoveDirection();  // 변경
    }

    private void SetDestination(Vector3 dest)
    {
        agent.SetDestination(dest);  //추가
        destination = dest;
        isMove = true;
    }

    // private void Move()
    private void LookMoveDirection()  // 변경
    {
        if (isMove)
        {
            // if (Vector3.Distance(destination, transform.position) <= 0.1f)
            if (agent.velocity.magnitude == 0.0f)  // 변경
            {
                isMove = false;
                return;
            }

            // var dir = destination - transform.position;
            var dir = new Vector3(agent.steeringTarget.x, transform.position.y, agent.steeringTarget.z) - transform.position;  // 변경
            character.transform.foward = dir;
        }
    }
}

 

그리고 스크립트 에디터로 돌아가서 AI 네임스페이스를 추가해주고 NavMeshAgent 변수를 만든다. 그 다음 Awake 함수에서 게임 오브젝트에 붙어있는 NavMeshAgent 컴포넌트를 가져와서 넣어준다.

 

컴포넌트를 가져온 다음에는 agent가 Character Root를 회전시키지 않도록 updateRotation을 false로 바꿔줍니다. 그리고 SetDestination 함수에서는 매개변수로 받아온 목표 위치를 agent.SetDestination 함수에 넣어준다.

 

이렇게 하면 NavMeshAgent가 자동으로 길을 찾아서 캐릭터를 이동하도록 만들어준다.

 

그러면 Move 함수의 원래 목적은 사라지는데, 대신 Move 함수는 BoxMan이 이동 방향을 쳐다보게 하는 기능을 해야한다.

 

변경되는 기능에 따라 함수의 이름은 LookMoveDirection으로 바꿔주면 좋다. 원래 코드는 BoxMan이 destination을 쳐다보게 되어있는데 이렇게 하면 캐릭터가 길을 돌아서 이동하는 와중에도 목적지만 쳐다봐서 부자연스럽게 움직이게 된다.

 

캐릭터는 목적지 대신에 자신이 이동하는 방향을 쳐다봐야 한다. direction을 구할 때 destination 대신 agent.steeringTarget을 사용해야 한다. 다만 아까 전에 캐릭터와 높이가 다른 목적지를 클릭 했을 때 캐릭터가 기울어졌던 것을 기억할 것이다. 캐릭터가 높이가 다른 곳을 쳐다보면서 기울어지는 것을 막기위해서 캐릭터가 agent의 steeringTarget 방향을 바라보되 높이는 캐릭터 자신의 높이를 바라보게 해야 한다. 그리고 도착 판정을 거리 대신에 agent의 속도로 변경하여 목적지에 도착한 경우면 함수를 종료하도록 만들어 준다.

 

이렇게 하면 캐릭터가 목적지에 도착한 다음 엉뚱한 방향을 바라보지 않게 될 것이다.

 

 

게임을 플레이시키고 우클릭으로 캐릭터를 이동시켜보면 잘 동작하는 것을 볼 수 있다. 벽 건너편을 클릭해도 제대로 돌아서 이동하고 벽 위를 클릭해도 캐릭터가 기울어지며 올라가려는 시도를 하지 않는 것을 알 수 있다.

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

+ Recent posts