리눅스에서 서버를 돌릴 경우 크래시 덤프를 뜨기가 힘들다.

이번에는 윈도우와 리눅스 둘 다 사용이 가능한 breakpad로 리눅스 덤프를 뜨는 방법을 알아보고자 한다.

 

리눅스는 ubuntu를 사용한다.

 


1. 필요한 라이브러리 인스톨

breakpad에서 필요한 라이브러리들을 미리 인스톨한다.

 

①GIT

 

 

② DEPOT TOOLS

break pad를 사용하기 위해 선행적으로 받아야한다.

 

사이트는 아래와 같다.

https://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/depot_tools_tutorial.html#_setting_up

 

 

1) git을 이용해 depot_tools를 받아온다

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

 

 

2) PATH 설정을 한다.

export PATH=/path/to/depot_tools:$PATH

여기에서 ['path/to']는 나의 PATH를 입력한다.

 

 

 

③ make / g++

 

sudo apt install make
sudo apt install g++

 

c++ 빌드 환경을 만들기위해 make와 g++을 설치해준다.

 

 

④ zlib1g-dev

 

sudo apt install zlib1g-dev

 

zlib1g dev버전으로 설치해준다.

 

 


2. BREAK PAD 설치

break pad의 홈페이지는 아래와 같다.

https://chromium.googlesource.com/breakpad/breakpad/

 

breakpad/breakpad - Git at Google

Breakpad Breakpad is a set of client and server components which implement a crash-reporting system. Getting started (from main) First, download depot_tools and ensure that they’re in your PATH.Create a new directory for checking out the source code (it

chromium.googlesource.com

 

 

 

git clone https://chromium.googlesource.com/breakpad/breakpad

 

git으로 breakpad를 clone해온다.

 

 

 

 

[

[Myproject]에 breakpad 폴더를 만든 후 fetch 해준다.

 

-. 폴더 만들기

mkdir breakpad
cd breakpad

 

-. fetch

fetch breakpad

 

 

 

 

fetch가 완료되었으면

src폴더로 이동해,

configure과 make를 이용해 breakpad를 빌드해준다

 

cd src
./configure
make

 

 


 

3. BREAK PAD사용

 

 

내부에서 null에러를 일으키는 임시 코드를 작성했다.

 

 

① exception handler import

 

② dump callback 작성

홈페이지에 있는 예시 그대로 작성하였다.

 

 

#include <iostream>
#include <thread>
#include "client/linux/handler/exception_handler.h"

static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor,
void* context, bool succeeded) {
  printf("Dump path: %s\n", descriptor.path());
  return succeeded;
}


void ThreadFunction(int* ptr)
{
        std::cout<<*ptr<<std::endl;
}

int main()
{
          google_breakpad::MinidumpDescriptor descriptor("/tmp");
          google_breakpad::ExceptionHandler eh(descriptor, NULL, dumpCallback, NULL, true, -1);
        //null PTR을 cout : nullptr에러를 일으킨다
        int* ptr = nullptr;
        std::thread threadTest(ThreadFunction, ptr);

        //쓰레드 종료시까지 대기
        threadTest.join();
        return 0;
}

 

③빌드

g++ -I/home/ubuntu/MyProject/breakpad/src/src BreakPadTest.cpp -o test.out -lbreakpad_client

 


 

4. 덤프 확인

 

크래시가 나면 덤프는 PATH설정한 곳으로 나오게 된다.

예시의 경우 /tmp에 덤프가 생성된다.

 

① 덤프 심볼릭 만들기

 

breakpad/src/src/tools/linux/dump_syms/dump_syms ./test.out > test.out.sym

 

덤프 심볼릭을 만든다.

이때 심볼릭은 빌드파일과 같은 이름으로 해야한다.

 

 

② 심볼릭 고유 코드 확인

 

head -n1 test.out.sym

head -n1을 이용하여 심볼릭이 어디를 나타내는지 확인한다

 

③ 심볼릭의 헤더와 같은 위치로 폴더 만들기

 

심볼릭의 헤더가 가리키는 위치에 폴더를 만든다.

그렇게 하지않으면 덤프에서 심볼릭을 찾을 수 없다.

mkdir -p ./symbols/test.out/C742B6BB5BEBFBC5F73889A0908E34610
mv test.out.sym ./symbols/test.out/C742B6BB5BEBFBC5F73889A0908E34610/

 

④ 크래시가 난 위치 확인

 

마지막으로,

breakpad/src/src/processor/minidump_stackwalk 68d64630-fceb-4e30-ba3ee3bc-e3aacc30.dmp ./symbols

 

를 이용하면 아래와 같은 결과가 나온다.

 

0 test.out의 ThreadFunction의 14번째 줄에서 크래시가 난 것을 알 수 있다.

 

 

 

정확히 크래시가 난 곳의 위치 확인이 가능하다.

 

 


윈도우에서는 visual studio에 덤프를 넣기만 해도 디버깅이 가능한 것으로 알고있다.

리눅스에서 뽑은 덤프도 visual studio에서 가능한지는 확인해볼 필요가 있어보인다.

2022.12.04 네이버 블로그에 업로드했던 글을 이전한 게시글입니다.

 

 

 

 

Unity로 MVC, MV(R)P, MVVM패턴을 구현해보려고한다.

MVC, MVP, MVVM에대한 설명은 많았지만 유니티로 구현한 설명은 적은것같아 공부겸 정리해보려고한다.

 

 

간단하게

체력 100인 캐릭터가 존재하며

Attack을 누르면 체력이 10씩 깎이며

Heal을 누르면 5씩 늘어나는 기능을 만들어보려고 한다.

 


[VIEW와 MODEL]

각 패턴의 공통인 View와 Model부터 작성한다

●Model

사용되는 데이터의 모임이다.

데이터와 데이터를 처리하는 부분을 Model 이라고 한다.

 

우선 HP가 필요하므로 Model에 HP를 작성한다.

public class Model
{
    private int _hp;
    public int HP { get=>_hp; set=>_hp = value; }
}

 

 

●VIEW

사용자에게 보여지는 UI부분이다.

현재 상태로는 버튼과 HP를 표현하는 텍스트가 있다.

변하는값은 HP를 표현하는 텍스트이므로 텍스트만 우선 작성한다

 

using UnityEngine;
using UnityEngine.UI;

public class View : MonoBehaviour
{
    [SerializeField]
    private Text _hpTxt;
}

 

 

 

 


[ MVC 패턴 ]

MVC패턴은 Model, View, Controller이다.

Model과 View가 연결되어있고, Controller에서 입력을 담당한다.

현재의 기능으로 설명하면,

  1. 버튼을 누르는 Action이 Controller로 들어온다.
  2. Controller에서 Model에게 HP값 변경을 요청한다.
  3. Model에서는 HP를 변경하고 이 값을 View에서 표시한다

●Controller

 

 

Controller에서는 버튼이 눌렸는지 확인만 담당한다.

 

 

using UnityEngine;
using UnityEngine.UI;

public class Controller : MonoBehaviour
{
    [SerializeField]
    private Button _attactButton;
    [SerializeField]
    private Button _healButton;

    [SerializeField]
    private Model _model;
    
    private void Start()
    {
        //Attack버튼이 눌리면 Model의 Attack을 호출한다
        _attactButton.onClick.AddListener(() =>
        {
            _model.Attack();
        });

        //Heal버튼이 눌리면 Model의 Heal을 호출한다
        _healButton.onClick.AddListener(() =>
        {
            _model.Heal();
        });
    }
}

 

 

●View

 

 

View에서는 받은 값을 사용자가 보는 화면에 표시만 한다.

 

using UnityEngine;
using UnityEngine.UI;

public class View : MonoBehaviour
{
    [SerializeField]
    private Text _hpTxt;

    public void ShowHPTxt(int hpValue)
    {
        _hpTxt.text = hpValue.ToString();
    }
}

 

●Model

Model에서 실제 데이터 처리와 View와의 연동을 담당한다.

using UnityEngine;

public class Model : MonoBehaviour
{
    private int _hp = 100;

    [SerializeField]
    private View _view;

    /// <summary>
    /// Attack버튼이 눌렸을 때
    /// hp - 10
    /// View에 표시요청
    /// </summary>
    public void Attack()
    {
        _hp -= 10;
        _view.ShowHPTxt(_hp);
    }

    /// <summary>
    /// Heal버튼이 눌렸을 때
    /// hp + 5
    /// View에 표시요청
    /// </summary>
    public void Heal()
    {
        _hp += 5;
        _view.ShowHPTxt(_hp);
    }
}

 


 

[ MVP 패턴 ]

MVP(MV(R)P)패턴은 Model, View, Presenter이다.

Model에서 데이터, View에서 화면을 담당하고 중간에 중개자 역할을 하는 Presenter가 존재한다.

 

 

 

현재의 기능으로 설명하면,

  1. 버튼을 누르는 Action이 View로 들어온다.
  2. View에서 Presenter에게 HP값 변경을 요청한다.
  3. Presenter는 Model에게 정보를 요청하고 받아온 정보를 View로 재송신한다

●View

이제 View에서 버튼 액션을 확인한다.

 

 

 

View의 버튼이 눌리면 Presenter에 값 변경을 요청한다.

 

using UnityEngine;
using UnityEngine.UI;

public class View : MonoBehaviour
{
    [SerializeField]
    private Text _hpTxt;
    [SerializeField]
    private Button _attactButton;
    [SerializeField]
    private Button _healButton;
    [SerializeField]
    private Presenter _presenter;
    private void Start()
    {
        //Attack버튼이 눌리면 Model의 Attack을 호출한다
        _attactButton.onClick.AddListener(() =>
        {
            _presenter.Attack();
        });

        //Heal버튼이 눌리면 Model의 Heal을 호출한다
        _healButton.onClick.AddListener(() =>
        {
            _presenter.Heal();
        });
    }

    public void ShowHPTxt(int hpValue)
    {
        _hpTxt.text = hpValue.ToString();
    }
}

 

●Presenter

 

Presenter에서는 Model의 값을 가지고있고,

View에서 요청한 값을 Model에서 받아와

다시 View 표시한다.

 

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

public class Presenter : MonoBehaviour
{
    private Model _model = new Model();
    [SerializeField]
    private View _view;

    public void Attack()
    {
        _view.ShowHPTxt(_model.Attack());
    }

    public void Heal()
    {
        _view.ShowHPTxt(_model.Heal());
    }
}

 

 

●Model

 

Model은 데이터 처리만을 담당한다.

public class Model
{
    private int _hp = 100;

    /// <summary>
    /// Attack버튼이 눌렸을 때
    /// hp - 10
    /// View에 표시요청
    /// </summary>
    public int Attack()
    {
        return _hp -= 10;
    }

    /// <summary>
    /// Heal버튼이 눌렸을 때
    /// hp + 5
    /// View에 표시요청
    /// </summary>
    public int Heal()
    {
        return _hp += 5;
    }
}

 

MVP방식의 장점은 View와 Model의 의존성이 줄어든 것이지만

반대로 View와 Presenter와의 의존성이 높다.

 

 

 


 

[ MVVM 패턴 ]

MVVM패턴은 Model, View, View Model이다.

Model에서 데이터, View에서 화면을 담당하고

Model의 데이터를 View에 맞게 파싱해주는 ViewModel이 존재한다.

 

View에 맞게 파싱해주는 역할이므로 View의 갯수만큼 ViewModel이 늘어난다.

 

 

현재의 기능으로 설명하면,

  1. 값을 받아오는 것 까지는 MVP패턴과 비슷하다.
  2. Presenter의 중재자역할 대신 ViewModel에서는 Model에서 받아온 값을 View에 맞게 파싱한다.

●ViewModel

 

이번엔 ViewModel에서 View용 데이터를 가지고있고,

Model에서 받아온 데이터를 View용으로 파싱한 후 이 값을 View에서 표시한다.

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

public class ViewModel : MonoBehaviour
{
    [SerializeField]
    private View _view;
    [SerializeField]
    private Model _model;

    private string _hpStr = string.Empty;
    public void Attack()
    {
        _hpStr = _model.Attack().ToString();
        _view.ShowHPTxt(_hpStr);
    }

    public void Heal()
    {
        _hpStr = _model.Heal().ToString();
        _view.ShowHPTxt(_hpStr);
    }
}

 

●Model

 

using UnityEngine;

public class Model : MonoBehaviour
{
    private int _hp = 100;

    /// <summary>
    /// Attack버튼이 눌렸을 때
    /// hp - 10
    /// View에 표시요청
    /// </summary>
    public int Attack()
    {
        return _hp -= 10;
    }

    /// <summary>
    /// Heal버튼이 눌렸을 때
    /// hp + 5
    /// View에 표시요청
    /// </summary>
    public int Heal()
    {
        return _hp += 5;
    }
}

 

●View

 

 

using UnityEngine;
using UnityEngine.UI;

public class View : MonoBehaviour
{
    [SerializeField]
    private Text _hpTxt;
    [SerializeField]
    private Button _attactButton;
    [SerializeField]
    private Button _healButton;
    private ViewModel _viewModel = new();
    private void Start()
    {
        //Attack버튼이 눌리면 Model의 Attack을 호출한다
        _attactButton.onClick.AddListener(() =>
        {
            _viewModel.Attack();
        });

        //Heal버튼이 눌리면 Model의 Heal을 호출한다
        _healButton.onClick.AddListener(() =>
        {
            _viewModel.Heal();
        });
    }

    public void ShowHPTxt(string hpValue)
    {
        _hpTxt.text = hpValue;
    }
}

 

 

Unity에서는 MVVM패턴이 잘맞지않는다는 의견이 있었다.

웹에 잘 맞는 패턴

디자인패턴은 개인의 자유이며 프로젝트의 요구사항에 따라 편한 패턴을 선택해서 사용하는것이 좋을 것 같다

2022.04.11 네이버 블로그에 올린 글을 이전한 게시글입니다.

 

 

UNITY로 칼리굴라 이펙트2 전투씬에 나오는 배경투명화 영상을 제작해보았다

 

 

우선 결과물.

 

 

 

칼리굴라의 느낌으로 카메라도 이동시켜 보았다.

 

 

영상원본은 망상감상대상연맹이다

이유는 알파추출이 쉬워보여서:)

 

https://youtu.be/8pGRdRhjX3o

 


1. Davinci Resolve로 배경투명 영상 만들기

 

유니티 공식홈페이지의 Videoplayer 설명을 보면

webm파일포멧과 VP8비디오 코덱이 있으면 비디오에 알파적용이 가능하다는 설명이 있다

https://docs.unity3d.com/kr/2020.3/Manual/VideoTransparency.html

 

우선 다빈치 리졸브를 이용하여 알파를 뺀다.

 

 

[다빈치 리졸브로 영상 알파 빼기]

하단의 [Color]탭에 이동한 후,

노드부분에 마우스 오른쪽 클릭 -> [Add Alpha Output]을 클릭한다.

[Add Alpha Output]을 클릭하면 파란색 노드가 추가된다

 

 

스포이드툴을 선택한 후 알파를 뺄 곳에(투명화를 할 곳에) 클릭한다.

 

선택한 부분의 알파를 뺄것이므로 반전을 눌러

선택한 부분의 알파값을 빼준다.

만약 크로마키 영상이 필요하다면 크로마키도 이와 같은 방법으로 진행한다.

렌더세팅

알파값을 함께 빼야하므로 [Export Alpha]를 체크해준다

[FFmpeg를 이용하여 포멧설정]

 

webm 포멧 코덱을 위해서는 FFmpeg를 사용해야한다

https://ffmpeg.org/download.html#build-windows

 

 

[설치폴더/bin]위치에 알파값을 뺀 영상을 넣는다

 

​[FramesToWebm.bat] 파일을 생성하여 실행시킨다

ffmpeg -i AlphaTest_Audio.mov -vcodec libvpx -auto-alt-ref 0 -codec:a libvorbis OutputAudio.webm

pause

 

-i AlphaTest_Audio.mov : 출력파일 이름

-vcodec libvpx : 사용코덱

-codec:a libvorbis OutputAudio.webm : 입력파일이름

[유니티에서 확인]

영상의 알파가 제대로 빠졌는지 확인한다

 

[Assets] - [Import New Asset]을 이용하여 만들어진 영상을 임포트한다

VideoPlayerPlayer로 플레이해보면 알파값이 잘 빠진것을 확인할 수 있다.

 


2. 굽어진(커브드) 캔버스 만들기

칼리굴라에선 둥글게 기울어진 캔버스를 사용하므로 기울어진 캔버스를 만들어보도록한다.

캔버스를 기울일 방법이 없으므로 다른 모델링을 이용하여 그 위에 매쉬랜더링을 하는 방법을 이용한다.

[Unity ProBuilder를 이용하여 커브드 캔버스 모델링 만들기]

 

[Package Manager]를 사용해 ProBuilder를 임포트해준다.

 

 

임포트에 성공했다면 [Tool]-[ProBuilder Window]로 프로빌더 윈도우를 활성화시킬 수 있다.

Unity ProBuilder는 유니티상에서 간단한 모델링이 가능하도록 지원해주는 기능이다

 

[New Shape]를 이용해 원뿔을 만들어준다

SideCount는 둥글게 보이도록 14로 설정했다

 

면 선택버튼을 누른 후 안쪽면을 제외한 바깥쪽 면들을 선택 -> [Delete Faces]를 클릭하여 내부면만 남겨둔다

 

 

 

 

내부의 면들만 남은 모습

 

 

 

 

UV에디터를 이용하여 UV를 정리해준다

 

 

 

 

처음 UV에디터를 켜면 UV가 정리되지않은 모습을 볼 수 있다

이를 정리해준다

[영상플레이용 머테리얼 생성하기]

 

 

 

[Create]-[Material]을 눌러 새로운 머테리얼을 추가한다

 

 

투명화를 지원해야하므로 Redering Mode를 [Transparent]로 설정해준다

 

 

 

 

VideoPlayer에 연결한다

 

 

결과물

영상의 알파값이 잘 빠진 모습이다

2022.02.15 네이버 블로그에 게시한 글을 이전한 글입니다.

 

 

 

스칼렛 스트링스의 메인 스킬 중 하나인 염력을 만들어보았다

 

 


[사용한 에셋]

●유니티쨩

- 메인 캐릭터 모델링

Unity-Chan! Model | 3D Characters | Unity Asset Store

●돌

- 염력을 사용할 오브젝트

Stone | 3D Exterior | Unity Asset Store

Stones | 3D Exterior | Unity Asset Store

●하이라이트(유료에셋)

- 염력객체에 붙일 하이라이트

Highlight Plus | Particles/Effects | Unity Asset Store

●지구

- 적 객체용. 지구로 한 이유는 친구의 추천:)

Stylized Earth | 3D Landscapes | Unity Asset Store

 


 

 

[가까운 돌 확인하기]

실시간으로 가까운 객체를 확인하여 플레이어로부터 가장 가까운 객체를 염력객체로 한다.

 

 

 

1. 염력객체 확인 범위 설정을 한다.

유니티쨩에게 원형 콜라이더를 하나 넣어 그 안에 있는 염력객체들을 대상으로 하도록 한다.

 

 

 

 

염력으로 사용할 수 있는 객체 리스트를 만들어

유니티쨩 범위 콜라이더안에 들어오면 리스트에 넣고, 범위밖으로 벗어나면 리스트에서 제외한다.

 

 

 

2. 리스트 내에서 가장 가까운 객체를 확인한다.

- Update와 Coroutine의 프레임차이가 크지않아서 그냥 Update에서 돌렸다.

 

 

System.Linq 의 Sort를 사용하여 가장 작은 값을 가장 앞에 오도록 한다.

 

private void Update()
    {
        //플레이어와 객체의 거리차이를 Dictionaty로 저장한다.
        var vistanceDic = new Dictionary<GameObject, float>();
        foreach (GameObject obj in _ableShootObjs)
        {
            vistanceDic.Add(obj, Vector3.Distance(transform.position, obj.transform.position));
        }

        if (vistanceDic.Count > 0)
        {
            //거리차가 가장 작은 객체를 찾는다.
            var minValue = vistanceDic.ToList();
            minValue.Sort((value1, value2) => value1.Value.CompareTo(value2.Value));

            //이전에 선택된 오브젝트가 있으면 선택해제한다.
            if (_nowShootObj != null && _nowShootObj != minValue[0].Key)
            {
                _nowShootObj.GetComponent<ShootItem>().EndHighlight();
            }

            //거리값의 차가 가장 적은 객체를 선택된 객체로 한다.
            _nowShootObj = minValue[0].Key;
            _nowShootObj.GetComponent<ShootItem>().PlayHighlight();
        }
        else
        {
            //이전에 선택된 오브젝트가 있으면 선택해제한다.
            if (_nowShootObj)
            {
                _nowShootObj.GetComponent<ShootItem>().EndHighlight();
                _nowShootObj = null;
            }
        }
    }

 

 

 


[게이지]

 

 

게이지 이미지는 포토샵으로 수제작했다...

 

1. 배경이미지

 

 

 

 

백그라운드 이미지.

게이지가 전혀 차지않았을때 보이는 이미지이다.

그저 백에 얹어두기만 하면 된다.

2. 게이지 마스크

 

 

 

게이지가 틀안에서 오르게 하기위한 마스크이다.

배경이미지랑 별 차이가 없긴하지만 나름 그라데이션을 뺀 이미지이다.

배경이미지를 그대로 사용해도 상관없다.

3. 게이지

 

 

 

 

실제로 오르는 게이지이다.

 

 

 

 

게이지바 함수.

마우스 이벤트는 각 염력객체에서 하고있다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class GaugeController : MonoBehaviour
{
    [SerializeField]
    private Image _gaugeImage;

    private float _gauge = 0f;
    private bool _isClicked = false;
    private bool _isSuccessWait = false;
    /// <summary>
    /// 마우스를 클릭할 시
    /// </summary>
    public void OnMouseDownEvent()
    {
        _isClicked = true;
        _isSuccessWait = false;
    }

    /// <summary>
    /// 마우스 클릭이 끝났을 때
    /// </summary>
    public void OnMouseUpEvent()
    {
        _isClicked = false;
        _gauge = 0f;
        _gaugeImage.fillAmount = _gauge;
    }

    /// <summary>
    /// 게이지가 다 찼는지 확인하는 함수
    /// </summary>
    public bool GetIsSuccessWait()
    {
        return _isSuccessWait;
    }

    private void Update()
    {
        if (!_isClicked)
            return;
        if (_gauge < 1.0f)
        {
            _gauge += Time.deltaTime;
        }
        
        if(_gauge >= 1.0f)
        {
            _gauge = 1.0f;
            _isSuccessWait = true;
        }

        _gaugeImage.fillAmount = _gauge;
    }
}

 

 

 


[염력스킬]

각 염력객체에 들어갈 스크립트에 작성.

 

 

마우스를 눌렀을 때, 객체를 위로 띄운 후, 움직이지않도록 설정해준다.

 

 

 

마우스를 떼었을 때, 게이지가 다 찼으면 적을 향해 날아가도록 한다.

 

 

마우스 클릭 이벤트는 게임메니저(싱글톤)에 넣어두어 이벤트로 불러오도록 하였다.

 

public class ShootItem : MonoBehaviour
{
    //각 객체의 게이지바
    [SerializeField]
    private GaugeController _gaugeController;

    //하이라이트(에셋)
    private HighlightEffect _highlightEffect;
    private Rigidbody _rigidbody;

    private bool _isSelected = false;
    private void Awake()
    {
        _highlightEffect = GetComponent<HighlightEffect>();
        _rigidbody = GetComponent<Rigidbody>();
        _gaugeController.gameObject.SetActive(false);
    }

    private void OnEnable()
    {
        GameManager.Instance.MouseDown.AddListener(MouseDownEvnet);
        GameManager.Instance.MouseUp.AddListener(MouseUpEvent);
    }

    #region 하이라이트
    public void PlayHighlight()
    {
        //하이라이트를 켠다
        if(!_highlightEffect)
            _highlightEffect = GetComponent<HighlightEffect>();
        _highlightEffect.highlighted = true;

        //가장 가까운 객체일 경우 게이지바를 보여준다
        _gaugeController.gameObject.SetActive(true);
        _isSelected = true;
    }

    public void EndHighlight()
    {
        //하이라이트를 끈다
        if (!_highlightEffect)
            _highlightEffect = GetComponent<HighlightEffect>();
        _highlightEffect.highlighted = false;

        _isSelected = false;
        //가장 가까운 객체가 아닐경우 게이지바를 없앤다
        _gaugeController.gameObject.SetActive(false);
    }
    #endregion

    #region 게이지

    public void MouseDownEvnet()
    {
        if (!_isSelected)
            return;

        //게이지바의 퍼센트를 올려준다.
        _gaugeController.OnMouseDownEvent();

        //중력을 끈다
        _rigidbody.useGravity = false;
        //마우스를 누르고있는 동안 객체가 움직이지않도록 한다.
        _rigidbody.isKinematic = true;
        //마우스를 누르고있는 동안 물체가 떠있도록 한다.(스칼렛스트링스에서 염력발동중엔 물체가 떠있다)
        _rigidbody.MovePosition(_rigidbody.position + Vector3.up);
    }

    public void MouseUpEvent()
    {
        //객체에 Kinematic 설정이 되어있다면 꺼준다
        if (_rigidbody.isKinematic)
            _rigidbody.isKinematic = false;

        if (_isSelected)
        {
            //만약 가장 가까운 객체이고, 게이지가 다 찼으면
            if (_gaugeController.GetIsSuccessWait())
            {
                //적의 위치를 향해 힘을 주어 객체가 적의 위치로 날아가도록한다.
                var direction = GameManager.Instance.EnemyTransform.position - transform.position; 
                _rigidbody.AddForce(direction.normalized * 10000f);
            }
            else
                _rigidbody.useGravity = true;
        }
        else
        {
            _rigidbody.useGravity = true;
        }
        _gaugeController.OnMouseUpEvent();
    }

    public void SetUseGravityTrue()
    {
        _rigidbody.useGravity = true;
    }
    #endregion
}

 

스칼렛스트링스를 하면서 가장 많이 느꼈던게 '염력스킬이 진짜 멋있다'는 것이었다.

전투하나는 A급이었던 스칼렛 스트링스의 스킬을 직접 도전해보니 나름 즐거운 시간이었던 것 같다.

다른장르의 멋진 스킬들도 한번씩 도전해보고싶다

2021.12.29에 네이버 블로그에 게시했던 글을 이전해 온 게시글입니다.

 

 

 

위 두가지 버전의 스와이프 메뉴를 만듭니다.

샘플 데이터는 개인적으로 좋아하는 페르소나5의 괴도단들로 제작했습니다 :)!


Step1. 씬 설정

 

 

게임뷰에서는 원형으로 돌아가는 연출이 되어야하므로 씬뷰에서 위에서 바라보면 캔버스를 가로지르는 원형이 되도록 만들어야합니다.

새 씬을 만들어 배경 이미지를 만들어줍니다.

이 이미지가 원을 반으로 나눠 뒤로 넘어간 캐릭터 목록을 가려줄것입니다.

 

Canvas를 카메라뷰로 바꾸고 카메라와 연결해줍니다.

Overlay : 캔버스가 화면에 맞게 사이즈가 변경되며 카메라와 관계없이 그려집니다.

항상 맨 앞에 그려지므로 지금처럼 캔버스앞에 다른 오브젝트가 그려져야하는 경우와 맞지않습니다.

Camera : 캔버스가 특정 카메라로부터 일정거리에 위치합니다.

WorldSpace : 캔버스를 오브젝트인것처럼 랜더링합니다. 캔버스의 위치를 포지션값으로 조정할 수 있습니다,


 

Step2. 캐릭터 목록 만들기

 

이번엔 캔버스밖에 빈 오브젝트를 생성합니다.

위치는 (0,0,0)으로 맞춰줍니다.

 

 

기본적으로 CanvasSwipe와 같지만 이번엔 Canvas의 밖에 있으므로

Image대신 SpriteRenderer를 사용해주며

Text대신 TextMesh를 사용해줍니다.

Text의 경우 Scale로 사이즈를 키우면 텍스트가 깨지므로

폰트 사이즈를 올리고 Scale을 줄이는 방향으로 하면 텍스트가 깨지지않습니다.

 

 

 

 

CanvasSwipe때와 똑같이 CharacterItem클래스를 작성해줍니다.

코드 전문은 페이지 아래에 있습니다.

 

 

이번에도 프리팹화 해줍니다.

 


Step3. 목록 완성하기

[개념]

캔버스를 기점으로 원형으로 쭉 돌립니다.

 

하늘색 선이 캔버스라고 할때, 캔버스를 기점으로 초록색 원을 그립니다

원의 위치로 캐릭터 프리팹들을 생성시켜주면 됩니다.

위에서 보면 같은 모양이 됩니다.

 

 

 

프리팹의 위치를 계산하는 방법입니다.

우선 프리팹 하나만 두고 생각을 해봅니다.

캔버스에서 캐릭터 프리팹을 향해 직선을 긋고 이 각도를 θ라고 합니다.

이 직선은 원의 반지름이 됩니다.

 

프리팹으로부터 캔버스로 직선을 내립니다.

캔버스로부터 직각이 되는곳에 빨간 직선을 내리면 반지름이 빗변이 되는 직각 삼각형이 됩니다.

 

 

캐릭터 프리팹의 위치 x,y는 (cosθ, sinθ)가 됩니다.

 

 

지금은 2차원이 아니라 3차원이므로 X값과 Z값을 이용해

Vector3 = (cosθ, 0, sinθ)

이됩니다.

 

 

이를 코드로 작성하면 이렇게됩니다.

nowAngle : 현재의 각도. θ

_radius : 반지름

Mathf.Cos(nowAngle) : 코사인 값

Mathf.Sin(nowAngle) : 사인값

캔버스를 기준으로 한바퀴를 돌려야하니 position값은 부모를 중심으로 localPosition으로 합니다.

 

마지막으로 각도 θ를 구하는 방법입니다.

원안에 캐릭터 갯수만큼을 넣어야하므로 (원 / 캐릭터 갯수)가 되므로 (360 / n)이 됩니다.

 

 

360도를 캐릭터 갯수로 나눠준 다음, 캐릭터의 수만큼 for문을 돌려 곱해줍니다.

이로서 각도 θ도 알게됩니다.

위치값은 호도법을 사용하므로 Mathf.Deg2Rad로 라디안 각도로 변환해줄 필요가 있습니다.

 

/// <summary>
    /// Json파일로부터 캐릭터 목록을 받아와 프리팹을 생성한다
    /// </summary>
    public void SetCharacters(Characters charactersList)
    {
        var characters = charactersList.characters;

        //원 360도를 캐릭터의 갯수로 나눈다
        _angle = 360f / characters.Length;

        //라디안 각도로 변경
        _angle *= Mathf.Deg2Rad;

        var nowAngle = 0f;
        for (int charactersIndex = 0; charactersIndex < charactersList.characters.Length; charactersIndex++)
        {
            //현재의 각도
            nowAngle = _angle * charactersIndex;

            var charactersObj = Instantiate(_characterItemPrefab, transform);

            //캐릭터 프리팹의 위치는 (cosθ, 0, sinθ)이다
            charactersObj.transform.localPosition = new Vector3(Mathf.Cos(nowAngle), 0, Mathf.Sin(nowAngle)) * _radius;

            var characterItem = charactersObj.GetComponent<CircleCharacterItem>();
            characterItem.SetCharacterItem(characters[charactersIndex]);

            _characterItems.Add(characterItem);
        }
    }

 

 

 

 

캔버스 밖에 GameObject를 생성하여 이름을 입력해줍니다.

Radius로 반지름을 설정하고, Scale을 조정해 원하는 모양을 맞춰줍니다.

 

 

위치는 캔버스의 위치와 맞도록 맞춰줍니다.

 

Scale을 이용해 원 모양을 변경하면 다른 느낌을 줄 수 있습니다.

 


Step4. 슬라이딩 기능 구현

이번엔 슬라이더 UI를 사용한게 아니므로 슬라이딩을 따로 구현해줘야합니다.

 

마우스 클릭이 확인되면 이전 마우스 위치와의 거리를 확인한 후, 그 차만큼 오브젝트를 돌려줍니다.

코드 전문은 아래에 있습니다.

이제 캐릭터들이 화면을 바라보도록 수정하겠습니다.

CircleCharacterItem에 LateUpdate문을 추가해줍니다.

LookAt을 이용하여 화면을 바라보게 할것입니다.

화면을 바라보게 하는 경우는 두가지가 있습니다.

 


① gameObject.transform.LookAt(Vector3.forward);
그냥 카메라를 바라보게 할 경우

 

 

 

② gameObject.transform.LookAt(gameObject.transform.position + Vector3.forward);
현재의 위치에서 앞쪽을 바라보게 할 경우

 

전 2번을 사용했습니다.

 

 


Step5. 선택기능 구현

 

 

각도를 하나하나 계산해서 선택된 캐릭터를 확인하는것보다 콜라이더를 이용하면 편하게 할 수 있으므로 이번엔 콜라이더를 사용했습니다.

 

 

반지름의 값과 스케일값은 마음대로 변경할 수 있으니

콜라이더의 위치는 변경된 값을 기준으로 설정해줍니다.

 

 

콜라이더에 들어오면 선택된것, 나갈경우 선택되지않은것으로 합니다.

저는 딱히 물리법칙이 필요한게 아니므로 콜라이더대신 Trigger를 사용했습니다.

 

 

원형 캐릭터 목록이 만들어졌습니다.

 


아래 코드 전문

[CircleSwiper.cs]

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

public class CircleSwiper : MonoBehaviour
{
    public float _radius = 10f;
    [SerializeField]
    private GameObject _characterItemPrefab;

    [SerializeField]
    private GameObject _collider;

    private List<CircleCharacterItem> _characterItems = new List<CircleCharacterItem>();

    private float _angle = 0f;
    private bool _isTouched = false;
    private float _lastPosition = 0f;
    /// <summary>
    /// Json파일로부터 캐릭터 목록을 받아와 프리팹을 생성한다
    /// </summary>
    public void SetCharacters(Characters charactersList)
    {
        var characters = charactersList.characters;

        //원 360도를 캐릭터의 갯수로 나눈다
        _angle = 360f / characters.Length;

        //라디안 각도로 변경
        _angle *= Mathf.Deg2Rad;

        var nowAngle = 0f;
        for (int charactersIndex = 0; charactersIndex < charactersList.characters.Length; charactersIndex++)
        {
            //현재의 각도
            nowAngle = _angle * charactersIndex;

            var charactersObj = Instantiate(_characterItemPrefab, transform);

            //캐릭터 프리팹의 위치는 (cosθ, 0, sinθ)이다
            charactersObj.transform.localPosition = new Vector3(Mathf.Cos(nowAngle), 0, Mathf.Sin(nowAngle)) * _radius;

            var characterItem = charactersObj.GetComponent<CircleCharacterItem>();
            characterItem.SetCharacterItem(characters[charactersIndex]);

            _characterItems.Add(characterItem);
        }

        //콜라이더의 위치 : 현재 중심의 위치 + (반지름 * 스케일 변경 값)
        _collider.transform.position = gameObject.transform.position + (transform.forward * -_radius * transform.localScale.z);
    }

    public CircleCharacterItem GetSelectedItem()
    {
        return _collider.GetComponent<CircleSelectCollider>().GetLastSelectedItem();
    }
    private void Update()
    {
        //마우스 클릭 확인 => 클릭일 경우 true
        if (Input.GetMouseButton(0))
        {
            _isTouched = true;
        }
        else
        {
            _isTouched = false;
            _lastPosition = 0f;
        }

        if (_isTouched == true)
        {
            //현재의 마우스 위치값 확인
            var nowPosition = Input.mousePosition.x;
            if (_lastPosition != 0f)
            {
                //현재의 위치값과 저번 위치값의 차를 확인한다
                var rotationRate = _lastPosition - nowPosition;
                //이를 rotation값에 더해준다
                gameObject.transform.eulerAngles = gameObject.transform.rotation.eulerAngles + new Vector3(0, rotationRate * 0.1f, 0);

            }
            _lastPosition = nowPosition;
        }
    }
}

 

 

[CircleCharacterItem.cs]

 

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

public class CircleCharacterItem : MonoBehaviour
{
    private Character _characterData;

    [SerializeField]
    private SpriteRenderer _characterPic;
    [SerializeField]
    private TextMesh _characterName;

    [SerializeField]
    private Animator _animator;

    public void SetCharacterItem(Character character)
    {
        _characterPic.sprite = Resources.Load<Sprite>("Image/" + character.PicName);
        _characterName.text = character.Name;
        _characterName.gameObject.SetActive(false);

        _characterData = character;
    }

    public void SetSelected()
    {
        _animator.SetTrigger("SetLarge");
        _characterName.gameObject.SetActive(true);
    }

    public void SetUnselected()
    {
        _animator.SetTrigger("SetSmall");
        _characterName.gameObject.SetActive(false);
    }

    public Character GetCharacterData()
    {
        return _characterData;
    }

    public Sprite GetPicSprite()
    {
        return _characterPic.sprite;
    }

    private void LateUpdate()
    {
        gameObject.transform.LookAt(gameObject.transform.position + Vector3.forward);
        //gameObject.transform.LookAt(Vector3.forward);
    }
}

 

[CircleSelectCollider.cs]

 

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

public class CircleSelectCollider : MonoBehaviour
{
    private CircleCharacterItem _lastSelectedItem;
    private void OnTriggerEnter(Collider collider)
    {
        var circleCharacterItem = collider.GetComponent<CircleCharacterItem>();
        if(circleCharacterItem)
        {
            circleCharacterItem.SetSelected();
            if(_lastSelectedItem != null && _lastSelectedItem != circleCharacterItem )
                _lastSelectedItem.SetUnselected();
            _lastSelectedItem = circleCharacterItem;
        }
    }

    private void OnTriggerExit(Collider collider)
    {
        var circleCharacterItem = collider.GetComponent<CircleCharacterItem>();
        if (circleCharacterItem)
        {
            circleCharacterItem.SetUnselected();
        }
    }

    public CircleCharacterItem GetLastSelectedItem()
    {
        return _lastSelectedItem;
    }
}

2012.12.29에 네이버 블로그에 게시했던 글을 이전해온 게시글입니다.

 

 

 

 

 

위 두가지 버전의 스와이프 메뉴를 만듭니다.

샘플 데이터는 개인적으로 좋아하는 페르소나5의 괴도단들로 제작했습니다 :)!

 


Step1. Scroll View 만들기

[UI]-[Scroll View]로 스크롤뷰를 생성해줍니다.

 

 

 

뒷배경을 깔끔하게 해주기위하여 배경도 만들었습니다.

이는 생략하셔도 됩니다.

이번엔 Vertical Scroll은 사용하지않으니 삭제해줍니다.


Step2. 캐릭터 목록 만들기

 

 

ScrollView - Viewport - Content 아래에 빈 오브젝트를 만들어줍니다.

 

 

 

 

빈 오브젝트를 CharacterItem으로 둔 후, 아래에 Image, Text를 만듭니다

빈 오브젝트의 크기는 이미지와 텍스트가 모두 들어갈정도의 크기로 합니다.

 

 

 

나중에 이부분이 될 예정입니다.

 

 

 

 

ScrollView - Viewport - Content 에

ContentSizeFilter 컴포넌트와 HorizontalLayoutGroup 컴포넌트를 추가합니다.

ContentSizeFilter : Content아래의 Children에 따라 Content의 크기를 조절해줍니다.

HorizontalLayoutGroup : 수직형으로 Children을 정렬해줍니다.

 

 

public void SetCharacterItem(Character character)
    {
        _characterPic.sprite = Resources.Load<Sprite>("Image/" + character.PicName);
        _characterName.text = character.Name;
    }

 

이제 캐릭터 목록용 class를 작성해줍니다.

JSON파일에서 가져온 데이터로, 이미지와 이름을 입력해줄것입니다.

(코드 전문은 페이지 제일 아래에 있습니다)

 

 

 

작성한 class를 캐릭터 목록의 컴포넌트로 추가해줍니다.

 

 

 

이를 끌어당겨 Prefab화 해주세요.

 


Step3. Json파일로부터 목록을 받아와 목록 완성하기

 

캐릭터 전체 목록을 총괄할 클래스를 만들어줍니다.

저는 CanvasSwiper로 이름지었습니다.

 

 

CanvasSwiper에 방금 만든 prefab과 Prefab를 복제할곳을 변수로 만들어줍니다.

 

 

SetCharacter를 작성합니다.

For문으로 캐릭터의 배열수만큼 돌리며

캐릭터의 배열수만큼 캐릭터 목록을 복제하고 정보를 넣습니다.

 

 

작성한 SetCharacter 함수를 JsonController의 Event에 연결합니다.

이로서 Json파일이 불러오면 SetCharacter를 불러오며

캐릭터 목록을 생성해줄겁니다.

 

 

 

캐릭터 목록이 불러와지는것을 확인할 수 있습니다.

 

 

 

캐릭터 목록이 중앙에서부터 시작되어 중앙에서 끝나도록 하기위하여

Content내의 HorizontalLayoutGroup의 Padding을 수정해줍니다

Left와 Right의 마진을 각각 스크린사이즈/2로 해주시고

Spacing은 보기 편하도록 해줍니다.

(저는 20으로 작성하였습니다)

 

 


Step4. 현재 선택된 캐릭터 만들기

             (캐릭터 컨트롤러)

 

 

캐릭터 선택에 대한 기본 개념은 이렇습니다

ⓛ. 캐릭터가 두개일 경우

②. 캐릭터가 세개일 경우

③. 캐릭터가 n개일 경우

A번째 캐릭터는 1/n * A 보다 크고, 1/n * (A+1) 보다 작게됩니다.

 

 

 

이를 코드로 작성하면 이와같이 됩니다.

 

for문을 돌리며 (1/n) * distanceIndex 보다 크고, (1/n) * (distanceIndex+1) 보다 작은경우 선택,

그 외의 경우는 선택 해제를 합니다.

 

 

 

 

slider의 Value값이 0과 1 사이가 아닐때가 있으므로

Mathf.Clamp를 사용하여 0과1사이로 바꿔줍니다.

Mathf.Clamp : 최소, 최댓값을 설정하여 value의 값이 그 사이를 넘지 않도록 해준다.

 

 

 

 

이제 값이 0과 1 사이이므로, 0인 경우는 첫번째, 1인경우는 마지막이 선택되도록 합니다.

 

 

마지막으로 scrollbar를 받아와 start에 리스너로 추가해줍니다.

이로서 스크롤바의 값이 변경되면 OnSliderValueChange 함수를 호출합니다.

 

 

/// <summary>
    /// 슬라이더 값이 변경되었을 때 불리는 함수
    /// </summary>
    /// <param name="value">변경된 슬라이더의 값</param>
    private void OnSliderValueChange(float value)
    {
        //value를 0과 1사이로
        value = Mathf.Clamp(value, 0f, 1f);
        for(int distanceIndex = 0; distanceIndex < _characterItems.Count; distanceIndex++)
        {
            //(1/n) * distanceIndex 보다 크고, (1/n) * (distanceIndex+1) 보다 작은경우 선택
            //그 외의 경우는 선택해제
            if (_distance * distanceIndex <= value)
            {
                if (_distance * (distanceIndex + 1f) > value)
                {
                    _characterItems[distanceIndex].SetSelected();
                    _selectedIndex = distanceIndex;
                    continue;
                }
            }

            _characterItems[distanceIndex].SetUnselected();
        }
        //distance가 0인 경우 첫번째 선택
        if (_distance == 0f)
        {
            _characterItems[0].SetSelected();
        }
        //distance가 1인 경우 마지막이 선택
        else if (_distance == 1f)
        {
            _characterItems[_characterItems.Count].SetSelected();
        }
    }

 


Step5. 현재 선택된 캐릭터 만들기 (각 캐릭터)

이제 캐릭터들이 선택되었을 때 선택되었음을 알 수 있게 애니메이션을 추가해줄 것입니다.

 

만들어두었던 prefab를 켜서 Animator 컴포넌트를 추가해줍니다.



 

Create-AnimatorController을 이용해 애니메이터 컨트롤러를 만들어줍니다.

 

 

 

Prefab의 애니메이터 컴포넌트에 이를 연결해줍니다.

 

 

 

 

선택됐을때 그림이 커지는 애니메이션을 만들어줍니다.

저는 그림 스케일을 0.7에서 1로 변경되도록 했습니다.

 

 

 

이를 애니메이터에 연결하여 트리거로 각각 설정해줍니다.

작아지는경우는 따로 애니를 만들 필요가 없이, 커지는 애니에서 속도만 -1로 설정해주면 됩니다.

 

 

 

마지막으로 선택됐을때와 선택되지 않았을때 각각의 애니가 플레이되도록 설정해주면 됩니다.

 

 

이로서 캔버스를 이용한 스와이프 기능이 완성되었습니다.


아래 코드 전문

[CanvasSwiper.cs]

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class CanvasSwiper : MonoBehaviour
{
    //캐릭터 목록 Prefab
    [SerializeField]
    private GameObject _characterItemPrefab;
    //캐릭터 목록이 생성될 곳
    [SerializeField]
    private Transform _swipeTransform;

    [SerializeField]
    private Scrollbar _slider;

    private List<CharacterItem> _characterItems = new List<CharacterItem>();
    private float _distance = 0f;

    private int _selectedIndex = 0;
    private void Start()
    {
        //슬라이더에 리스너를 추가하여 값이 변경되면 OnSliderValueChange함수를 호출하도록 함
        _slider.onValueChanged.AddListener(OnSliderValueChange);
    }

    /// <summary>
    /// 캐릭터 목록 생성
    /// </summary>
    public void SetCharacters(Characters charactersList)
    {
        var characters = charactersList.characters;
        for (int charactersIndex = 0; charactersIndex < charactersList.characters.Length; charactersIndex++)
        {
            //prefab을 복제한다
            var charactersObj = Instantiate(_characterItemPrefab, _swipeTransform);

            //Json에서 받아온 캐릭터 정보를 캐릭터 프리팹에 넣어준다
            var characterItem = charactersObj.GetComponent<CharacterItem>();
            characterItem.SetCharacterItem(characters[charactersIndex]);

            //캐릭터 목록에 추가한다
            _characterItems.Add(characterItem);
        }

        //각 value들 사이의 거릿값
        //A번째 캐릭터는 1/n * A 보다 크고, 1/n * (A+1) 보다 작게된다
        //1/n을 매번 계산하면 코스트가 들어가므로 _distance라는 변수에 저장
        _distance = 1f / _characterItems.Count;

        //첫번째 캐릭터를 선택으로 변경해둔다
        _characterItems[0].GetComponent<CharacterItem>().SetSelected();
    }

    /// <summary>
    /// 선택되어있는 캐릭터를 return함
    /// </summary>
    public CharacterItem GetSelectedItem()
    {
        return _characterItems[_selectedIndex];
    }

    /// <summary>
    /// 슬라이더 값이 변경되었을 때 불리는 함수
    /// </summary>
    /// <param name="value">변경된 슬라이더의 값</param>
    private void OnSliderValueChange(float value)
    {
        //value를 0과 1사이로
        value = Mathf.Clamp(value, 0f, 1f);
        for(int distanceIndex = 0; distanceIndex < _characterItems.Count; distanceIndex++)
        {
            //(1/n) * distanceIndex 보다 크고, (1/n) * (distanceIndex+1) 보다 작은경우 선택
            //그 외의 경우는 선택해제
            if (_distance * distanceIndex <= value)
            {
                if (_distance * (distanceIndex + 1f) > value)
                {
                    _characterItems[distanceIndex].SetSelected();
                    _selectedIndex = distanceIndex;
                    continue;
                }
            }

            _characterItems[distanceIndex].SetUnselected();
        }
        //distance가 0인 경우 첫번째 선택
        if (_distance == 0f)
        {
            _characterItems[0].SetSelected();
        }
        //distance가 1인 경우 마지막이 선택
        else if (_distance == 1f)
        {
            _characterItems[_characterItems.Count].SetSelected();
        }
    }
}

 

[CharacterItem.cs]

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class CharacterItem : MonoBehaviour
{
    private Character _characterData;

    [SerializeField]
    private Image _characterPic;
    [SerializeField]
    private Text _characterName;

    [SerializeField]
    private Animator _animator;

    private bool _isSelected = false;
    public void SetCharacterItem(Character character)
    {
        //캐릭터 이미지
        _characterPic.sprite = Resources.Load<Sprite>("Image/" + character.PicName);
        //캐릭터 이름
        _characterName.text = character.Name;
        _characterName.gameObject.SetActive(false);

        _characterData = character;
    }

    /// <summary>
    /// 캐릭터가 선택됨
    /// </summary>
    public void SetSelected()
    {
        if (_isSelected)
            return;
        _isSelected = true;

        _animator.SetTrigger("SetLarge");
        _characterName.gameObject.SetActive(true);
    }

    /// <summary>
    /// 캐릭터가 선택되지않음
    /// </summary>
    public void SetUnselected()
    {
        if (!_isSelected)
            return;
        _isSelected = false;

        _animator.SetTrigger("SetSmall");
        _characterName.gameObject.SetActive(false);
    }

    /// <summary>
    /// 자신이 가진 캐릭터의 정보를 return해줌
    /// </summary>
    public Character GetCharacterData()
    {
        return _characterData;
    }

    /// <summary>
    /// 자신이 가진 캐릭터의 이미지를 return해줌
    /// 리소스에서 이미지를 두세번씩 가져오는것을 방지하기 위함 
    /// </summary>
    public Sprite GetPicSprite()
    {
        return _characterPic.sprite;
    }
}

 

 


뒷 게시글에서 계속됩니다.

2021.12.28에 네이버 블로그에 게시했던 글을 이전해온 게시글입니다.

 

 

위 두가지 버전의 스와이프 메뉴를 만듭니다.

샘플 데이터는 개인적으로 좋아하는 페르소나5의 괴도단들로 제작했습니다 :)!

 

 


Step1. Json파일 만들기

저는 편의상 Json파일로 제작하였지만, Json이 아닌 CSV, XML등을 사용하셔도 됩니다.

아래는 Json파일에대한 설명이 있습니다.

캐릭터 정보를 담을 Json파일을 제작합니다.

 

 

만들어진 Json파일을 불러오기 편하도록 Resouces폴더 아래에 둡니다.

 

 

Resources/Image 에는 캐릭터들의 이미지를 넣어두었습니다.

이미지명은 Json파일의 PicName과 일치하도록 합니다.

 

 

 


 

Step2. Character Class 만들기

 

JsonUtility로 바로 파싱할 수 있도록 Character클래스를 생성해줍니다.

클래스명 위에 [Serializable]을 작성하여 직렬화가 가능하도록 해줍니다.

(직렬화 되어있지 않으면 JsonUtility를 사용할 수 없습니다)

Json파일과 동일한 형태로 변수를 만들어줍니다.

 

또한 여러개의 캐릭터를 동시에 사용해야하므로 편하게 사용하기 위하여 이를 배열로 묶어주었습니다.

 

[Character.cs]

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

[Serializable]
public class Character
{
    public int Index;
    public string Name;
    public string CV;
    public string PicName;
}
[Serializable]
public class Characters
{
    public Character[] characters;
}

 


Step3. JsonFile Load, 파싱

Resources.Load를 사용해 파일을 불러온 후, JsonUtility를 이용해 Character 클래스로 파싱해줍니다.

파일로드중 프로젝트가 멈추는걸 방지하기위해 비동기로 제작하였습니다.

Resources.LoadAsync : 리소스 파일을 비동기로 불러옴

resourceRequest.completed : 파일로드가 끝났을 때 실행되는 이벤트

JsonUtility.FromJson<Characters> : JSON으로 이루어진 String 문자열을 Characters로 변경하여 return함

로드가 끝나면 빨간 네모안의 코드가 실행됩니다.

 


Step4. 로드가 끝나면 불러올 이벤트 제작

step4를 진행하면 인스펙터 창에서 로드가 끝났을때 불러올 함수를 지정할 수 있습니다.

 

 

 

빈 클래스를 만들어, UnityEvent<T>를 상속받도록 합니다.

이때 <T>에는 return할 class를 넣습니다.

(실제 사용할 데이터)

 

이제 이를 사용하면 인스펙터창에 불러올 이벤트를 설정할 수 있습니다.

 

 

 

 

이벤트를 불러올곳에서 Invoke를 해주면 이벤트를 불러옵니다.

?.Invoke : 이벤트가 Null인지 확인한 후, Null이 아닐경우만 Call하기

 

[JsonController.cs]

 

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

[Serializable]
public class JsonLoadedEvent : UnityEvent<Characters>{}

public class JsonController : MonoBehaviour
{
    public JsonLoadedEvent JsonLoadEvent;

    private void Start()
    {
        LoadJsonFile();
    }

    /// <summary>
    /// JSON파일로부터 캐릭터 정보를 받아온다
    /// </summary>
    public void LoadJsonFile()
    {
        var resourceRequest = Resources.LoadAsync<TextAsset>("Character");
        
        //로드가 끝나면 completed뒤의 익명함수를 불러온다
        resourceRequest.completed += (operation) =>
        {
            //json파일로부터 받아온 값을 TextAsset에 넣는다
            var rsltText = resourceRequest.asset as TextAsset;

            //TextAsset의 string데이터를 class로 파싱한다
            var characters = JsonUtility.FromJson<Characters>(rsltText.text);

            //연결된 이벤트들을 부른다
            JsonLoadEvent?.Invoke(characters);
        };
    }
}

 


뒷 게시글에서 계속됩니다.

2021.08.27 네이버 블로그 게시글을 이전해 온 게시글입니다.


Animator를 이용하여 실행시킨 애니메이션이 끝났을 때를 감지하고 Callback함수를 만들도록한다.

 

 


1) 기본 개념

StateMachineBehaviour 사용하면 애니메이션의 현재 상태를 알 수 있다

▽공식문서

https://docs.unity3d.com/Manual/StateMachineBehaviours.html

 


2) 구현

① 우선 테스트를 위한 씬으로, 큐브만 존재하는 씬을 만들었다.

큐브에 이동 애니메이션을 넣어 애니메이션이 끝나는 순간 콜백을 불러오도록 할것이다

 

 

 

 

② 큐브에 테스트 애니메이터를 추가하였다.

아무것도없는 빈 애니메이션을 Idle로두고,

실제 실행할 애니메이션을 연결하여

StartAnim트리거가 발동하면 실행시키도록 해둔다

 

 

 

③ Animation StateMachineBehavior 작성

스크립트 파일을 새로 만들어 StateMachineBehaviour 을 상속하도록 한다

 

 

 

StateMachineBehaviour 을 상속하게되면 OnStateEnter와 OnStateExit를 사용할 수 있다

OnStateEnter : 애니메이터에서 해당 애니메이션 state에 들어옴

OnStateExit : 애니메이터에서 해당 애니메이션 state에서 나감(이때 콜백을 불러올 예정)

④ 작성한 Animation StateMachineBehavior를 애니메이터에 연결

 

 

애니메이터의 콜백을 넣을 애니메이션을 클릭하면, AddBehavior를 이용해 만들어둔 Animation StateMachineBehavior을 추가할 수 있다

⑤ 실제 작동시킬 스크립트 작성

키보드 A버튼을 누를 경우, 애니메이션이 실행하도록하는 스크립트를 작성하였다

 

 

 

 

위의 스크립트에 Callback용 코드를 추가해준다.

GetBehaviour<TestAnimationBehavior>() 를 이용하여 작성했던 StateMachineBehavior에 접근할 수 있다.

앞서 작성했던 SetCallback() 함수에 콜백할 함수를 넣어 콜백함수를 연결시킨다.

⑥ 큐브에는 애니메이터와 실제 작동할 스크립트만 존재한다

 

 


3) 구동결과

 

애니메이션이 끝남과 동시에 Callback함수가 불려오는것을 확인할 수 있다.

 


4) 소스코드

- [TestAnimationBehavior.cs] 애니메이터에 들어갈 animatoionBehavior

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

public class TestAnimationBehavior : StateMachineBehaviour
{
    public delegate void AnimationCallback();
    private AnimationCallback _callback;

    /// <summary>
    /// Callback설정
    /// </summary>
    public void SetCallback(AnimationCallback callback)
    {
        _callback = callback;
    }
    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    }
    public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        //Callback이 설정되어있다면 불러온다
        _callback?.Invoke();

        //콜백을 삭제해준다
        _callback -= _callback;
    }
}

 

 

- TestAnimation.cs

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

public class TestAnimation : MonoBehaviour
{
    private Animator _animator;
    // Start is called before the first frame update
    void Start()
    {
        _animator = GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        if(Input.GetKeyDown(KeyCode.A))
        {
            _animator.SetTrigger("StartAnim");
            _animator.GetBehaviour<TestAnimationBehavior>().SetCallback(CallbackMethod);
        }
    }

    /// <summary>
    /// 애니가 끝나고 불러올 함수
    /// </summary>
    public void CallbackMethod()
    {
        Debug.Log("애니메이션이 끝났습니다");
    }
}

 

 

2021.08.11 네이버 블로그 게시글을 이전해 온 게시글입니다.


유니티 버튼의 OnClick 이벤트는 클릭했을 때(또는 눌렀을 때) 한번만 발동한다.

평범한 버튼을 클릭했을때 숫자가 올라가는 코드를 작성하였다

 

 

 

이 경우 버튼을 꾹 누르고있어도 9에서 숫자가 오르지않는것을 볼 수 있다.

이때, 버튼을 꾹 누르고있을때에도 숫자가 올라가게 하고싶다면

유니티의 버튼 대신

IPointerDownHandler, IPointerUpHandler 인터페이스를 사용해야한다.

 

 

클래스의 상속에 IPointerDownHandler, IPointerUpHandler 인터페이스를 추가한다.

각 인터페이스를 구현하고 버튼이 눌렸는지 아닌지를 확인하기위한 bool변수를 선언한다.

 

 

이제 버튼대신 이 함수만을 컴포넌트로 넣으면 꾹 눌러서 사용하는 버튼으로 사용가능하다.

 

 

 

 

 

마우스를 누르고있으면 계속하여 숫자가 오르는것을 볼 수 있다

 

 

 


코드 전문

 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class ButtonTest : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
    [SerializeField]
    private Text _text;
    private int myNum = 0;

    private bool _isButtonDown = false;

    private void Update()
    {
        if(_isButtonDown)
            NumUp();
    }
    public void NumUp()
    {
        myNum++;
        _text.text = myNum.ToString();
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        _isButtonDown = true;
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        _isButtonDown = false;
    }
}

 

 

 


UnityAction기능과 합하면 버튼을 눌렀을때 불려오는 함수를 매번 바꿀수도있다.

 

2021.08.09 네이버 블로그에 게시했던 글을 이전해온 게시글입니다.

 


리스트를 Clear 할 경우와 = new List로 초기화해주는 경우의 차이점에 대해 알아보았다.

우선 테스트용 스크립트를 아래와 같이 짜보았다.

 

 

리스트를 처음에는 test1을 넣고,

clear 또는 new List를 한 후

test2를 넣어보았다.

 

 

 

Test1과 Test2의 차이는 하나는 Clear, 하나는 새로 New List를 해준 것 밖에 없다.

 

 

 

결과는 다음과 같다

clear를 한 경우는 둘 다 test2가 들어갔으며,

new List를 한 경우는 test1, test2가 들어갔다.

 

 


 

좀 더 명확하게 보기 위하여 '안녕하세요' 'Hellow World!'로 재테스트 해보았다.

 

 

 

 


이렇게 되는 원인으로는 C# 도큐먼트에서 찾을 수 있다

https://docs.microsoft.com/ko-kr/dotnet/api/system.collections.generic.list-1.clear?view=net-5.0

 

 

List.clear의 경우 다른 개체에 한 참조도 해제되므로, 미리 넣어두었던 test1도 해제되어버리는 것이다.

따라서 후에 넣은 test2가 들어가는 것.

clear가 작업시간이 더 적게 소요되므로 작업시간을 줄이려면 clear로 사용하고

참조를 지우고싶지않거나 값의 재사용이 필요할 경우는 new 를 사용하는것이 좋다

 

 

 

2021.06.23 네이버 블로그에 게시한 글을 이전해 온 게시글입니다.

 


유니티 게임 실행 중 나오는 로그들을 파일로 추출하거나 게임뷰에 표시하는 기능을 만들어보았다.

 

 

 

 

화면에 표시되는 로그들과 만들어진 로그파일.

 


로그 핸들러

유니티에서 로그를 찍으면 불려오는 이벤트(델리게이트)는

Application.logMessageReceived
Application.logMessageReceivedThreaded

 

가 존재한다.

Application.logMessageReceived 는 메인스레드에서 로그를 찍을 때 불리며,

Application.logMessageReceivedThreaded 는 서브스레드의 로그도 함께 찍힌다.

Application.logMessageReceived += 함수명;

으로 로그를 찍을때마다 함수를 불리게한다.

 

 


로그 파일 추출

로그 파일은 FileStream을 이용했다.

로그파일을 만드는데에 메인쓰레드를 멈추게해선 안된다고 생각하여 아래 모두 비동기식으로 처리하였다.

1) 폴더 존재 여부 확인

await Task.Run(() => { 
                cancelToken.ThrowIfCancellationRequested();
                Directory.CreateDirectory(LOG_FOLDER_PATH);
            });

 

Directory.CreateDirectory로 폴더를 생성해준다.

2) (폴더가 존재할 경우) n일 이전에 생성된 파일 삭제

DateTime.Today.AddDays(-_oldFileDate).ToString("yyyyMMdd")과 CompareTo를 이용해 생성된 날짜를 비교한 후, n일 이전에 생성된 로그파일은 삭제해준다.

await Task.Run(() =>
                {
                    string deleteDayStr = DateTime.Today.AddDays(-_oldFileDate).ToString("yyyyMMdd");
                    var files = new DirectoryInfo(LOG_FOLDER_PATH).GetFiles();
                    for (int fileIndex = 0; fileIndex < files.Length; fileIndex++)
                    {
                        if (deleteDayStr.CompareTo(files[fileIndex].LastWriteTime.ToString("yyyyMMdd")) > 0)
                        {
                            Debug.Log("로그파일 " + files[fileIndex].FullName + "을 삭제합니다.");
                            files[fileIndex].Attributes = FileAttributes.Normal;
                            cancelToken.ThrowIfCancellationRequested();
                            files[fileIndex].Delete();
                            cancelToken.ThrowIfCancellationRequested();
                        }
                    }
                });

 

3) 오늘일자 로그 생성 및 로그 작성

오늘일자 로그 생성

await Task.Run(() =>
                {
                    cancelToken.ThrowIfCancellationRequested();
                    File.Create(LOG_FILE_PATH);
                });

 

로그 작성

string logMessage = _messageQue.Dequeue();
                byte[] encordMessage = Encoding.Unicode.GetBytes(logMessage);
                using (FileStream fileStream = new FileStream(LOG_FILE_PATH, FileMode.Append))
                {

                    await fileStream.WriteAsync(encordMessage, 0, encordMessage.Length, cancelToken);
                }

 

 


로그 표시

 

 

Contents에 VerticalLayoutGroup와 ContentSizeFitter를 넣은 후,

LogText를 복제해서 사용하도록 한다.

LogText안에는 LogViewTextItem.cs를 컴포넌트로 추가해준다.

서서히 사라지는 효과를 내주기 위해서 text의 컬러의 알파값을 fadeout해준다.

private IEnumerator StartFade()
        {
            yield return new WaitForSeconds(2f);

            _currentTime = 0f;
            while (_currentTime <= _timeToFade)
            {
                _currentTime += Time.deltaTime;
                _LogText.color = Color.Lerp(_startColor, _targetColor, _currentTime / _timeToFade);
                yield return null;
            }
            
            DestroySelf();
        }

 


 

소스코드 전문

1) LogController.cs

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Log;
using UnityEngine.UI;

public class LogController : MonoBehaviour
{
    public delegate void LogReceivedDelegate(string logMessage, string stackTrace, LogType type);
    private static LogReceivedDelegate _logReceived;
    public static void LogReceivedDel(LogReceivedDelegate logReceivedDelegate) { _logReceived += logReceivedDelegate; }

    [Header("로그파일 추출 여부")]
    public bool _isCreateLogFile = true;
    [Header("화면에 로그 표시할지 여부")]
    public bool _isShowLogScreen = true;
    [Header("쓰레드 로그도 표시할지 여부")]
    public bool _isThreadLogShown = false;

    [SerializeField]
    private GameObject _logFileObj;
    [SerializeField]
    private GameObject _logView;


    private void Awake()
    {
        Application.logMessageReceived += LogReceivedHandler;
        Application.logMessageReceivedThreaded += LogReceivedHandlerThread;
        if (_isCreateLogFile)
            _logFileObj.SetActive(true);
        if (_isShowLogScreen)
            _logView.SetActive(true);
    }

    private void LogReceivedHandler(string logMessage, string stackTrace, LogType type)
    {
        _logReceived(logMessage, stackTrace, type);
    }

    private void LogReceivedHandlerThread(string logMessage, string stackTrace, LogType type)
    {
        if(_isThreadLogShown)
            _logReceived(logMessage, stackTrace, type);
    }

    private void OnDestroy()
    {
        Application.logMessageReceived -= LogReceivedHandler;
        Application.logMessageReceivedThreaded -= LogReceivedHandlerThread;

        _logReceived -= _logReceived;
    }
}

 

2) LogFile.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

namespace Log
{
    public class LogFile : MonoBehaviour
    {
        public int _oldFileDate = 30;

        public string _logFormat = @"
[{0}] {1} {2}
{3}";

        private string LOG_FILE_PATH = "";
        private string LOG_FOLDER_PATH = "";

        private Queue<string> _messageQue = new Queue<string>();
        private bool _isLocked = false;

        CancellationTokenSource _cancleToken = new CancellationTokenSource();
        
        async void Start()
        {
            if (Application.platform.Equals(RuntimePlatform.Android))
                LOG_FOLDER_PATH = Application.persistentDataPath + "/Log";
            else
                LOG_FOLDER_PATH = Application.dataPath + "/.." +"/Log";


            LOG_FILE_PATH = LOG_FOLDER_PATH + "/log_" + DateTime.Today.ToString("yyyyMMdd") + ".txt";

            if (!Directory.Exists(LOG_FOLDER_PATH))
                await CreateFolder(_cancleToken.Token);
            else
                await DeleteOldFiles(_cancleToken.Token);

            if (!File.Exists(LOG_FILE_PATH))
                await CreateLogFile(_cancleToken.Token);

            LogController.LogReceivedDel(EnqueLogQue);

            _isLocked = false;
        }

        private async void Update()
        {
            if (_messageQue.Count > 0)
                await WriteLogMessage(_cancleToken.Token);
        }

        public void EnqueLogQue(string logMessage, string stackTrace, LogType type)
        {
            _messageQue.Enqueue(String.Format(_logFormat, type.ToString(),DateTime.Now.ToString("HH:mm:ss"),logMessage,stackTrace));
            
        }
        private async Task CreateFolder(CancellationToken cancelToken)
        {
            Debug.Log("로그 파일 폴더를 생성합니다. 생성위치 : " + LOG_FOLDER_PATH);
            await Task.Run(() => { 
                cancelToken.ThrowIfCancellationRequested();
                Directory.CreateDirectory(LOG_FOLDER_PATH);
            });
        }
        
        private async Task DeleteOldFiles(CancellationToken cancelToken)
        {
            try
            {
                await Task.Run(() =>
                {
                    string deleteDayStr = DateTime.Today.AddDays(-_oldFileDate).ToString("yyyyMMdd");
                    var files = new DirectoryInfo(LOG_FOLDER_PATH).GetFiles();
                    for (int fileIndex = 0; fileIndex < files.Length; fileIndex++)
                    {
                        if (deleteDayStr.CompareTo(files[fileIndex].LastWriteTime.ToString("yyyyMMdd")) > 0)
                        {
                            Debug.Log("로그파일 " + files[fileIndex].FullName + "을 삭제합니다.");
                            files[fileIndex].Attributes = FileAttributes.Normal;
                            cancelToken.ThrowIfCancellationRequested();
                            files[fileIndex].Delete();
                            cancelToken.ThrowIfCancellationRequested();
                        }
                    }
                });
            }
            catch(Exception e)
            {
                Debug.LogError(e.Message);
            }
        }

        private async Task CreateLogFile(CancellationToken cancelToken)
        {
            try
            {
                Debug.Log("로그 파일을 생성합니다. 생성위치 : " + LOG_FILE_PATH);
                await Task.Run(() =>
                {
                    cancelToken.ThrowIfCancellationRequested();
                    File.Create(LOG_FILE_PATH);
                });
            }
            catch (Exception e)
            {
                Debug.LogError(e.Message);
            }
        }

        private async Task WriteLogMessage(CancellationToken cancelToken)
        {
            if (_isLocked)
                return;

            try
            {
                _isLocked = true;

                string logMessage = _messageQue.Dequeue();
                byte[] encordMessage = Encoding.Unicode.GetBytes(logMessage);
                using (FileStream fileStream = new FileStream(LOG_FILE_PATH, FileMode.Append))
                {

                    await fileStream.WriteAsync(encordMessage, 0, encordMessage.Length, cancelToken);
                }
                
            }
            catch (Exception e)
            {
                Debug.LogError(e.Message);
            }
            finally
            {
                _isLocked = false;
            }

        }
        private void OnDestroy()
        {
            _cancleToken.Cancel();
        }
    }
}

 

 

3) LogView.cs

 

 

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

namespace Log
{
    public class LogView : MonoBehaviour
    {
        [SerializeField]
        private GameObject _logItemObj;
        [SerializeField]
        private Transform _logItemTransform;

        void Start()
        {
            LogController.LogReceivedDel(CreateLogItem);
        }

        public void CreateLogItem(string logMessage, string stackTrace, LogType type)
        {
            var logVeiwTextItem = Instantiate(_logItemObj, _logItemTransform).GetComponent<LogViewTextItem>();
            logVeiwTextItem.StartViewText(string.Format("[{0}] {1}", type.ToString(), logMessage));
        }

    }
}

 

 

 

4)LogViewTextItem.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

namespace Log
{
    public class LogViewTextItem : MonoBehaviour
    {
        private Text _LogText;

        private float _timeToFade = 2f;
        private float _currentTime = 0f;

        private Color _startColor;
        private Color _targetColor;
        public void StartViewText(string logMessage)
        {
            _LogText = GetComponent<Text>();

            _startColor = _LogText.color;
            _targetColor = _startColor;
            _targetColor.a = 0;

            _LogText.text = logMessage;

            gameObject.SetActive(true);

            StartCoroutine(nameof(StartFade));
        }

        private IEnumerator StartFade()
        {
            yield return new WaitForSeconds(2f);

            _currentTime = 0f;
            while (_currentTime <= _timeToFade)
            {
                _currentTime += Time.deltaTime;
                _LogText.color = Color.Lerp(_startColor, _targetColor, _currentTime / _timeToFade);
                yield return null;
            }
            
            DestroySelf();
        }

        private void DestroySelf()
        {
            Destroy(gameObject);
        }

    }

}

2021.06.23 네이버에 게시했던 글을 이전해온 게시글입니다.

 


 

비트세이버 등에서 사용된, 메시를 원하는 모양대로 자르는 기능을 만들어보려고 한다.

VR 요리게임에 칼로 과일등을 자르면 자른 단면대로 새로운 메시가 만들어지도록 하는게 목표이다.

 

 

 

 

현재 상태.

▼ 이곳을 참고하여 만들었다.

https://qiita.com/edo_m18/items/31961cd19fd19e09b675


기본 개념

한 도형의 메시는 삼각형으로 이루어져있다.

 

잘리는 선에 의해 잘리는 삼각형 메시는

잘리는 선을 기준으로 1개와 2개로 나뉜다.

 

나뉘어진 두개의 점은 또 다른 두개의 삼각형으로 나누는게 가능하다.

이런식으로 메시를 나누어 새로 생성하는 방식을 반복한다.

 


함수 설명

_bladePlane = new Plane(
                targetGameObj.transform.InverseTransformDirection(transform.right),
                targetGameObj.transform.InverseTransformPoint(transform.position)
            );

 

blade 를 자르는 칼이라고 한다면, blade의 위치에 맞춰 Plane을 생성한다.

이 Plane을 기준으로 도형의 Mesh를 자른다.

 

 

isLeftSide = _bladePlane.GetSide(targetMesh.vertices[triangles[triangleIndex + verticesIndex]]);
                    if (isLeftSide)
                    {
                        leftPoints.Add(new MeshPointCreater(
                                        targetMesh.vertices[triangles[triangleIndex + verticesIndex]],
                                        targetMesh.uv[triangles[triangleIndex + verticesIndex]],
                                        targetMesh.normals[triangles[triangleIndex + verticesIndex]],
                                        targetMesh.tangents[triangles[triangleIndex + verticesIndex]]
                        ));
                    }
                    else
                    {
                        rightPoints.Add(new MeshPointCreater(
                                        targetMesh.vertices[triangles[triangleIndex + verticesIndex]],
                                        targetMesh.uv[triangles[triangleIndex + verticesIndex]],
                                        targetMesh.normals[triangles[triangleIndex + verticesIndex]],
                                        targetMesh.tangents[triangles[triangleIndex + verticesIndex]]
                        ));
                    }

 

bladePlane과 현재 지정하고 있는 점의 위치를 비교해본 후 , 현재 점의 위치가 bladePlane보다 왼쪽일 경우 left, 오른쪽일 경우 right에 넣는다.

 

switch (leftPoints.Count)
                {
                    //모든 점이 오른쪽에 존재할 경우
                    case 0:
                        _rightMeshCreater.SetPoints(new List<MeshPointCreater>() { rightPoints[0], rightPoints[1], rightPoints[2] }, subMeshIndex);
                        break;
                    //왼쪽에 존재하는 점이 하나, 오른쪽에 존재하는 점이 두개일 경우
                    case 1:
                        //혼자 떨어진 점을 0번으로 한다.
                        yield return CheckAndSetPoints(new MeshPointCreater[3] { leftPoints[0], rightPoints[0], rightPoints[1] }, true, subMeshIndex);
                        break;
                    //왼쪽에 존재하는 점이 두개, 오른쪽에 존재하는 점이 하나일 경우
                    case 2:
                        //혼자 떨어진 점을 0번으로 한다.
                        yield return CheckAndSetPoints(new MeshPointCreater[3] { rightPoints[0], leftPoints[0], leftPoints[1] }, false, subMeshIndex);
                        break;
                    //모든 점이 왼쪽에 존재하는 경우
                    case 3:
                        _leftMeshCreater.SetPoints(new List<MeshPointCreater>() { leftPoints[0], leftPoints[1], leftPoints[2] }, subMeshIndex);
                        break;
                }

 

이렇게 왼쪽 오른쪽 나눈 점 세개를 확인해본 후,

왼쪽 점이 3개일 경우 점이 모두 왼쪽에 있으므로 그냥 왼쪽 오브젝트에 3개의 점으로 삼각형 메시를 그린다.

오른쪽 점이 3개일 경우 점이 모두 오른쪽에 있으므로 그냥 오른쪽 오브젝트에 3개의 점으로 삼각형 메시를 그린다.

오른쪽과 왼쪽이 각각 1,2/2,1 으로 나뉘어져 있을 경우 기본개념에서 설명한 대로 점을 나눈 후 삼각형 3개로 나눈다.

//점 i번과 0번의 (왼쪽과 오른쪽의) 거리를 구한다
            Vector3 pointDistance = pointCreaters[faceIndex + 1].Verticles - pointCreaters[0].Verticles;
            _bladePlane.Raycast(new Ray(pointCreaters[0].Verticles, pointDistance.normalized), out distance2Plane);

            float nomalizedDistance = distance2Plane / pointDistance.magnitude;

            normalizedPoints[faceIndex] = new MeshPointCreater(
                    Vector3.Lerp(pointCreaters[0].Verticles, pointCreaters[faceIndex + 1].Verticles, nomalizedDistance),
                    Vector3.Lerp(pointCreaters[0].UVs, pointCreaters[faceIndex + 1].UVs, nomalizedDistance),
                    Vector3.Lerp(pointCreaters[0].Normals, pointCreaters[faceIndex + 1].Normals, nomalizedDistance),
                    Vector3.Lerp(pointCreaters[0].Tangents, pointCreaters[faceIndex + 1].Tangents, nomalizedDistance)
                );

 

blade로부터 각 점의 거리를 파악한 후 거리에 맞게 점, UV, 노멀을 새로 생길 점에 설정한다.

 

 

Vector3 crossProduct = Vector3.Cross(drawPointDummy[1].Verticles - drawPointDummy[0].Verticles, drawPointDummy[2].Verticles - drawPointDummy[0].Verticles);

        Vector3 normalAverage = (drawPointDummy[0].Normals + drawPointDummy[1].Normals + drawPointDummy[2].Normals) / 3f;

        float dotProduct = Vector3.Dot(normalAverage, crossProduct);

        if(dotProduct < 0)
        {
            MeshPointCreater normalChange = new MeshPointCreater();
            normalChange = drawPointDummy[0];
            drawPointDummy[0] = drawPointDummy[2];
            drawPointDummy[2] = normalChange;
        }

 

새로 생길 면이 바라보는 방향을 확인한다.

점들을 외적한 값을 내적하여 내적값이 0보다 작을 경우 면이 뒤집혀있다고 생각,

면을 뒤집도록 한다.

 

for (int newPointIndex = 0; newPointIndex< _newPoints.Count; newPointIndex++)
        {
            centerPoint += _newPoints[newPointIndex];
        }
        centerPoint /= _newPoints.Count;
        Vector3[] myPoint = new Vector3[2];

        Vector3 bladeUpVector = new Vector3(_bladePlane.normal.y, _bladePlane.normal.x, _bladePlane.normal.z);
        Vector3 bladeLeftVector = Vector3.Cross(_bladePlane.normal, bladeUpVector);

        for(int newPointIndex = 0; newPointIndex < _newPoints.Count; newPointIndex += 2)
        {
            myPoint[0] = _newPoints[newPointIndex];
            myPoint[1] = _newPoints[newPointIndex + 1];

            newUV = new Vector2(0.5f + Vector2.Dot(myPoint[0] - centerPoint, bladeLeftVector), 0.5f + Vector2.Dot(myPoint[0] - centerPoint, bladeUpVector));

            MeshPointCreater[] normalChecked = CheckNormalMesh(new MeshPointCreater[3] {  
                new MeshPointCreater(centerPoint, newUV, -_bladePlane.normal, Vector4.zero),
                new MeshPointCreater(myPoint[0], newUV, -_bladePlane.normal, Vector4.zero),
                new MeshPointCreater(myPoint[1], newUV, -_bladePlane.normal, Vector4.zero) });

            _leftMeshCreater.SetPoints(new List<MeshPointCreater>() {normalChecked[0], normalChecked[1], normalChecked[2] }, subMeshIndex);

            normalChecked = CheckNormalMesh(new MeshPointCreater[3] {  
                new MeshPointCreater(centerPoint, newUV, _bladePlane.normal, Vector4.zero),
                new MeshPointCreater(myPoint[0], newUV, _bladePlane.normal, Vector4.zero),
                new MeshPointCreater(myPoint[1], newUV, _bladePlane.normal, Vector4.zero) });

            _rightMeshCreater.SetPoints(new List<MeshPointCreater>() { normalChecked[0], normalChecked[1], normalChecked[2] }, subMeshIndex);
        }

 

이번에 자르면서 새로 생긴 점들에 대한 면을 만들어준다.

새로 생긴 점들을 n으로 나눈 값으로 중앙 점이 어딘지 찾고, 중앙점에 맞춰서 삼각형을 새로 그린다

 

 

 

 


소스 코드 전문

●BladeMeshCutter.cs

- 칼에 붙이는 컴포넌트

- rigidbody/Collider필요

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

public class BladeMeshCutter : MonoBehaviour
{
    private Plane _bladePlane = new Plane();


    //실질적으로 매시를 만드는 애들을 좌우 각각 생성한다
    private MeshCreater _leftMeshCreater = new MeshCreater();
    private MeshCreater _rightMeshCreater = new MeshCreater();

    private List<Vector3> _newPoints = new List<Vector3>();
    /// <summary>
    /// 특정 오브젝트를 반으로 가른다
    /// </summary>
    /// <param name="targetGameObj">잘리는 대상 게임 오브젝트</param>
    public void StartMeshCut(GameObject targetGameObj)
    {
        targetGameObj.GetComponent<Rigidbody>().isKinematic = true;
        var targetMeshCutController = targetGameObj.GetComponent<MeshCutTarget>();
        if(!targetMeshCutController)
        {
            Debug.Log("MeshCutter::자르는게 불가능한 오브젝트입니다.");
            return;
        }

        //잘리는 방향으로 plane을 생성한다
        _bladePlane = new Plane(
                targetGameObj.transform.InverseTransformDirection(transform.right),
                targetGameObj.transform.InverseTransformPoint(transform.position)
            );

        StartCoroutine(MeshCutCor(targetGameObj));
    }

    private IEnumerator MeshCutCor(GameObject targetGameObj)
    {
        var targetMeshCutController = targetGameObj.GetComponent<MeshCutTarget>();
        Mesh targetMesh = targetMeshCutController.Mesh;
        //잘리는 점이 왼쪽 오브젝트에 속하는가 오른쪽 오브젝트에 속하는가를 판별
        bool isLeftSide = false;

        // 왼쪽과 오른쪽 오브젝트를 그릴 점
        List<MeshPointCreater> leftPoints, rightPoints;

        //실질적으로 매시를 만드는 애들을 좌우 각각 생성한다
        _leftMeshCreater = new MeshCreater();
        _rightMeshCreater = new MeshCreater();

        //서브매시의 수만큼 루프하며 자를 단면의 점을 추출한다
        for (int subMeshIndex = 0; subMeshIndex < targetMesh.subMeshCount; subMeshIndex++)
        {
            var triangles = targetMesh.GetTriangles(subMeshIndex);

            //서브매시의 삼각형 수만큼 루프하며 점이 왼쪽 오브젝트가 속하는가 왼쪽 오브젝트에 속하는가를 구분한다
            leftPoints = new List<MeshPointCreater>();
            rightPoints = new List<MeshPointCreater>();

            _newPoints = new List<Vector3>();

            for (int triangleIndex = 0; triangleIndex < triangles.Length; triangleIndex += 3)
            {
                leftPoints = new List<MeshPointCreater>();
                rightPoints = new List<MeshPointCreater>();
                for (int verticesIndex = 0; verticesIndex < 3; verticesIndex++)
                {
                    isLeftSide = _bladePlane.GetSide(targetMesh.vertices[triangles[triangleIndex + verticesIndex]]);
                    if (isLeftSide)
                    {
                        leftPoints.Add(new MeshPointCreater(
                                        targetMesh.vertices[triangles[triangleIndex + verticesIndex]],
                                        targetMesh.uv[triangles[triangleIndex + verticesIndex]],
                                        targetMesh.normals[triangles[triangleIndex + verticesIndex]],
                                        targetMesh.tangents[triangles[triangleIndex + verticesIndex]]
                        ));
                    }
                    else
                    {
                        rightPoints.Add(new MeshPointCreater(
                                        targetMesh.vertices[triangles[triangleIndex + verticesIndex]],
                                        targetMesh.uv[triangles[triangleIndex + verticesIndex]],
                                        targetMesh.normals[triangles[triangleIndex + verticesIndex]],
                                        targetMesh.tangents[triangles[triangleIndex + verticesIndex]]
                        ));
                    }
                }

                //모두 오른쪽에 있거나 왼쪽에 있을 경우는 그대로 삼각형 단면을 생성하도록 하고
                //점이 왼쪽과 오른쪽에 나누어져 있을 경우 갈라서 두개의 삼각형 단면을 생성한다
                switch (leftPoints.Count)
                {
                    //모든 점이 오른쪽에 존재할 경우
                    case 0:
                        _rightMeshCreater.SetPoints(new List<MeshPointCreater>() { rightPoints[0], rightPoints[1], rightPoints[2] }, subMeshIndex);
                        break;
                    //왼쪽에 존재하는 점이 하나, 오른쪽에 존재하는 점이 두개일 경우
                    case 1:
                        //혼자 떨어진 점을 0번으로 한다.
                        yield return CheckAndSetPoints(new MeshPointCreater[3] { leftPoints[0], rightPoints[0], rightPoints[1] }, true, subMeshIndex);
                        break;
                    //왼쪽에 존재하는 점이 두개, 오른쪽에 존재하는 점이 하나일 경우
                    case 2:
                        //혼자 떨어진 점을 0번으로 한다.
                        yield return CheckAndSetPoints(new MeshPointCreater[3] { rightPoints[0], leftPoints[0], leftPoints[1] }, false, subMeshIndex);
                        break;
                    //모든 점이 왼쪽에 존재하는 경우
                    case 3:
                        _leftMeshCreater.SetPoints(new List<MeshPointCreater>() { leftPoints[0], leftPoints[1], leftPoints[2] }, subMeshIndex);

 

 

●MeshPointCreater.cs

점들의 정보 모음

 

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

public class MeshPointCreater
{
    /// <summary>
    /// 점의 위치
    /// </summary>
    private Vector3 _vertices;
    /// <summary>
    /// 점 근처의 UV정보
    /// </summary>
    private Vector2 _uvs;
    /// <summary>
    /// 점 근처의 노말맵 정보
    /// </summary>
    private Vector3 _normals;
    /// <summary>
    /// 점의 접선정보
    /// </summary>
    private Vector4 _tangents;

    public Vector3 Verticles { get => _vertices; }

    public Vector2 UVs { get => _uvs; }
    public Vector3 Normals { get => _normals; }
    public Vector4 Tangents { get => _tangents; }
    public MeshPointCreater(Vector3 vertices, Vector2 uvs, Vector3 normals, Vector4 tangents)
    {
        _vertices = vertices;
        _uvs = uvs;
        _normals = normals;
        _tangents = tangents;
    }

    public MeshPointCreater()
    {

    }
}

 

 

●MeshCreater.cs

- 실질적으로 메시를 그리기위한 정보 모음

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

public class MeshCreater
{
    private List<Vector3> _vertices = new List<Vector3>();
    private List<Vector3> _normals = new List<Vector3>();
    private List<Vector2> _uvs = new List<Vector2>();
    private List<Vector4> _tangents = new List<Vector4>();

    private List<List<int>> _subMeshIndexs = new List<List<int>>();

    public int GetVerticlesListCount()
    {
        return _vertices.Count;
    }

    /// <summary>
    /// 메시가 될 점들을 저장한다
    /// </summary>
    /// <param name="point">점</param>
    /// <param name="subMeshIndex">서브 메시 no.</param>
    public void SetPoints(List<MeshPointCreater> point, int subMeshIndex)
    {
        int vertCount = _vertices.Count;

        if (_subMeshIndexs.Count < subMeshIndex + 1)
        {
            for (int count = _subMeshIndexs.Count; count < subMeshIndex + 1; count++)
            {
                _subMeshIndexs.Add(new List<int>());
            }
        }

        for (int index = 0; index < 3; index++)
        {
            _vertices.Add(point[index].Verticles);
            _normals.Add(point[index].Normals);
            _uvs.Add(point[index].UVs);

            if (point[index].Verticles != null)
            {
                _tangents.Add(point[index].Tangents);
            }
            _subMeshIndexs[subMeshIndex].Add(vertCount + index);
        }
    }

    public Mesh CreateMesh()
    {
        Mesh mesh = new Mesh();
        mesh.SetVertices(_vertices);
        mesh.SetNormals(_normals);
        mesh.SetUVs(0, _uvs);
        mesh.SetUVs(1, _uvs);

        if (_tangents.Count > 1)
            mesh.SetTangents(_tangents);

        mesh.subMeshCount = _subMeshIndexs.Count;

        for (int subMeshIndex = 0; subMeshIndex < _subMeshIndexs.Count; subMeshIndex++)
        {
            mesh.SetTriangles(_subMeshIndexs[subMeshIndex], subMeshIndex);
        }

        return mesh;
    }
}

 

 


개선할 점

두번째 시도부터는 내부 메시(새롭게 만들어지는 메시)가 제대로 계산되지 않음을 발견했다.

이를 개선해 나갈 예정이다.

 

 

 

2021.06.17 네이버 블로그에 게시했던 글을 이전한 게시글입니다.

 


 

일 관련으로 가까운 점을 검색하는 법을 찾고 있던 와중에

얼핏 보기에 비슷해 보이는 알고리즘 세 개가 있어 공부 겸 비교해보았다.


데이크스트라 알고리즘(Dijkstra algorithm)

최단경로 알고리즘 또는 다익스트라 알고리즘이라고도 불린다.

지도 등에서 최단 경로를 찾을 때 사용된다.

 

출발점에서 도착점까지 최단경로로 가야할때,

갈 수 있는 루트가 A,B,C가 있다고 한다.

 

 

① 우선 갈 수 있는 모든 루트를 무한으로 잡는다.

② 이웃하고있는 모든 루트를 방문하여 거리를 계산한다.

출발점에서 인접하고있는 모든 루트는 A와 B가 된다.

따라서 현재의 최단거리를 가진 점은 A이며 비용은 각각 11, 12가 된다

③ 가장 거리가 적은 루트를 저장해둔 다음 계속 이웃한 점의 거리를 구한다.

현재는 A가 가장 가깝기에 A를 최단점으로 지정한다.

최단점이 지정되었다면, 다시 루프를 돌아 인접 점들의 거리를 확인한다.

 

B에서 C를 경우하는 경우가 A를 통하는 경유보다 거리가 짧기때문에

최단 거리는 B,C를 거치는 루트가 된다.

 

이 작업을 반복하여 최단 거리를 구한다.

 

 


플러드 필 알고리즘(FloodFill)

다차원 배열의 특정 칸과 연결된 영역을 찾는 알고리즘.

그림판/포토샵의 "페인트 도구"에 자주 사용된다.

 

 

① 보라색 테두리 안의 영역을 녹색으로 채우고싶다고 한다.

 

② 우선 특정 영역을 기준으로 잡는다.

 

③ 영역의 상,하,좌,우를 큐에 넣는다.

④ 다음 큐의 위치로 이동한다

 

⑤ 역시 상하좌우를 큐에 넣는다.

그러나 하는 이미 색이 칠해져 있으므로 큐에 넣지않아도 된다.

 

⑥ 이를 반복하여 색을 채워나간다

 

 

 


최근접쌍 문제(Closet Pair Problem)

 

아래와 같이 무작위로 점이 존재할 때, 가장 가까운 점 두개를 고를 때 사용된다.

일반적으로 점 하나하나를 비교하면 시간이 오래걸리므로, 비교하는 점의 갯수를 줄여 시간을 줄이는 방식이다.

 

아래의 점의 집합이 있다고 한다.

 

① 점의 갯수를 반으로 나눌 수 있는 점 하나를 정한다.

위치가 중앙이 아니어도 되기에 점 P[n/2]로 정한다.

② 나눠진 그룹 내에서 점들끼리 거리를 비교하여 가장 거리가 짧은 점들을 구한다

 

중앙점을 기점으로 나눈 점들끼리 길이를 비교한다.

 

③ 그룹내에서 가장 거리가 짧은 점 한쌍을 각각 지정한다.

예시에선 임의로 두 쌍을 정해 각각 거리가 2, 3이라고한다.

④ 마지막으로 양쪽그룹에 있는 점들중에 거리값이 더 작은 쌍이 있는지 찾는다.

 

 


 

세 알고리즘의 비교

데이크스트라, 플러드필, 최근접점쌍 알고리즘을 비교하면 아래와 같다

 

● 데이크스트라

- 출발점과 도착점이 정해져있을 경우 유용하다.

- 도착점까지 이동 중, 여러 점을 지나가는 '경유'를 생각해야 할 경우 사용된다.

●플러드 필

- 특정 영역을 채워야할 때 사용하면 유용하다.

- 게임의 경우 파이프에 물이 지나가는 길이나 미로 전체에 가스가 퍼진다거나할때 물 또는 가스가 이동하는 루트 설정에 사용될 수 있다.

●최근접점쌍

- 많은 점들 중 가장 거리가 가까운 한쌍을 찾는데 유용하다.

- 하지만 3D좌표상에는 z축의 거리도 생각해야하므로 번거로운 과정을 거쳐야할 수 있다.

 

 

 

2021.05.13 네이버 블로그에 게시한 글을 이전한 게시글입니다.

 


 

날씨 API 정보를 받아와 UNITY에서 사용하도록 한다.

날씨 API는 OpenWeather에서 제공해주는 API를 사용하였다.

https://openweathermap.org/

 

Сurrent weather and forecast - OpenWeatherMap

Access current weather data for any location on Earth including over 200,000 cities! The data is frequently updated based on the global and local weather models, satellites, radars and a vast network of weather stations. how to obtain APIs (subscriptions w

openweathermap.org

 


1. API 설정

OpenWeather의 API를 사용하기 위해서는 API용 ID가 필요하다.

API id를 얻는 방법은 아래와 같다.

 

 

우선 가입을 한 후,

[API]탭에 들어가면 사용가능한 API들을 볼 수 있다.

현재 날씨의 정보가 필요하므로 Current Weather Data로 선택하였다.

 

 

사용하는 범위에 맞게 요금제를 선택할 수 있다.

 

구독한 후, MyApikey 페이지에서 확인하면 API key를 확인할 수 있다.

 


 

+ API 확인

현재 날씨의 API 주소는 아래와 같다.

(공식 사이트의 doc에서 확인가능하다)

 

서울의 현재 날씨를 나타내는 API 주소이다.

appid= 뒤에 본인의 API id를 입력하면 현재 날씨 정보를 얻을 수 있다.

 

 


2. API 취득 소스코드 작성

 

UnityWebRequest.Get(API_ADDRESS);
webRequest.SendWebRequest();

 

UnityWebRequest를 이용하여 네트워크로부터 정보를 얻어오는 코드이다.

 

json데이터 중, base는 변수명으로 사용할 수 없으므로 변수명을 바꿔서 사용해준다.

 


 

 

네트워크로부터 API를 받아오는 동안 프로그램이 대기하지않도록 코루틴으로 작성하여 대기를 줄여준다.

 

 

 


2. 소스코드

 

[WeatherController.cs]

 

using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;


public class WeatherController : MonoBehaviour
{
    //API 주소
    //===============================================
    public const string API_ADDRESS = @"api.openweathermap.org/data/2.5/weather?q=Seoul&appid=e";
    //===============================================

    //날씨 데이터가 다운로드되면 CallBack으로 필요한 함수로 돌아간다
    public delegate void WeatherDataCallback(WeatherData weatherData);

    //다운로드된 날씨 데이터. 중복 다운로드를 막기위하여 저장해둔다
    private WeatherData _weatherData;

    /// <summary>
    /// API로부터 날씨 데이터를 받아온다
    /// </summary>
    public void GetWeather(WeatherDataCallback callback)
    {
        //현재의 날씨 데이터가 없다면 API로부터 받아온다
        if (_weatherData == null)
        {
            StartCoroutine(CoGetWeather(callback));
        }
        else
        {
            //현재의 날씨 데이터가 존재한다면 그 날씨데이터를 그대로 사용한다
            callback(_weatherData);
        }
    }
    
    /// <summary>
    /// 날씨 API로부터 정보를 받아온다
    /// </summary>
    /// <param name="callback"></param>
    /// <returns></returns>
    private IEnumerator CoGetWeather(WeatherDataCallback callback)
    {
        Debug.Log("날씨 정보를 다운로드합니다");

        var webRequest = UnityWebRequest.Get(API_ADDRESS);
        yield return webRequest.SendWebRequest();

        //만약 에러가 있을 경우
        if(webRequest.isHttpError || webRequest.isNetworkError)
        {
            Debug.Log(webRequest.error);
            yield break;
        }

        //다운로드 완료
        var downloadedTxt = webRequest.downloadHandler.text;

        Debug.Log("날씨 정보가 다운로드 되었습니다! : " + downloadedTxt);

        //유니티 언어와 겹치므로 base를 사용할 수 없기때문에 Replace가 필요하다
        string weatherStr = downloadedTxt.Replace("base", "station");

        _weatherData = JsonUtility.FromJson<WeatherData>(weatherStr);
        callback(_weatherData);
    }
}

 

[WeatherData.cs] - json파싱용 클라스

 

using System;

[Serializable]
public class WeatherData
{
    public Coord coord;
    public Weather[] weather;
    public string station;
    public Main main;
    public int visibility;
    public Wind wind;
    public Clouds clouds;
    public int dt;
    public Sys sys;
    public int id;
    public string name;
    public int cod;
}

[Serializable]
public class Coord
{
    public float lon;
    public float lat;
}

[Serializable]
public class Weather
{
    public int id;
    public string main;
    public string description;
    public string icon;
}

[Serializable]
public class Wind
{
    public float speed;
    public float deg;
}

[Serializable]
public class Main
{
    public float temp;
    public int pressure;
    public int humidity;
    public float temp_min;
    public float temp_max;
}

[Serializable]
public class Clouds
{
    public int all;
}

[Serializable]
public class Sys
{
    public int type;
    public int id;
    public float message;
    public string country;
    public int sunrise;
    public int sunset;
}

2021.05.10 네이버 블로그에 게시한 글을 이전하였습니다.

 

 

언리얼엔진 C++로 외부의 exe 파일을 실행하고 끄는 코드를 작성하기

*소스코드는 페이지 제일 아래에 있습니다

 


1) UE4에서 외부파일 실행시키기

 

 

c++ 액터 클래스를 생성한다.

레벨블루프린트에서 테스트하기위하여 액터클래스로 생성했다

 

헤더파일(.h)

const FString& AppPath 로 지정해서 Path를 블루프린트에서 수정할 수 있게하였다.

 

 

소스파일(.cpp)

FPlatformProcess::CreateProc

을 사용하면 지정위치의 파일을 실행시키는 것이 가능하다.

 

Path에 로컬 패스를 넣어 언리얼에서 유니티 빌드파일을 실행할 수 있다.

 


2) 외부파일 실행 성공 여부 알아보기

 

 

헤더파일(.h)

실행 결과를 확인하기위하여 bool& Result를 추가했다.

 

 

result값에 따라, 성공/실패를 보여주도록 하였다.

 

 

성공적으로 실행되었다.


3) 관리자의 권한이 필요한 파일 실행

 

일반 파일은 실행하는데에 문제가 없었는데, 관리자권한이 필요한 파일(예)Setup파일)은 실패하는것을 발견했다.

 

그래서, 관리자 권한이 필요한지 한번 더 확인하기로하였다.

 

소스파일(.cpp)

FPlatformProcess::ExecElevatedProcess

를 사용하면 관리자의 권한이 필요한 경우, 관리자의 권한을 묻게된다

 


4) 실행한 프로그램 프로세스 ID 저장

후에 프로그램을 종료하기위하여 현재 실행시킨 프로그램파일의 프로세스 ID를 저장한다

 

헤더파일(.h)

int& ProcessId를 추가하여 프로세스 ID를 블루프린트상에서 저장할 수 있게한다

소스파일(.cpp)

 


5) 실행중인 프로그램 종료하기

 

헤더파일(.h)

저장해뒀던 프로세스ID를 파라미터로 넘긴다.

프로세스 ID가 실행중인지 확인하는 bool값을 return 받는다

 

 

소스코드(.cpp)

FPlatformProcess::OpenProcess 함수를 이용하면 프로세스ID로부터 ProcHandle을 받아올 수 있다.

이때 받아온 ProcHandle을 FPlatformProcess::IsProcRunning 를 이용해 실행중인지 확인한다.

프로세스가 실행중이라면 FPlatformProcess::TerminateProc 를 이용하여 프로그램을 종료한다.

마지막으로, FPlatformProcess::CloseProc 를 이용하여 ProcHandle도 닫아주도록한다.

 

 


5) 최종 테스트

 

프로그램을 실행시키기위한 블루프린트 코드

 

프로그램을 종료시키기위한 블루프린트 코드

↓[헤더파일.h]

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "RunExternalTest.generated.h"

UCLASS()
class NDIBROADCASTER_API ARunExternalTest : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ARunExternalTest();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	UFUNCTION(BlueprintCallable, Category = "ControlExternalFiles")
		void RunExternalExeFile(const FString& AppPath, bool& Result, int& ProcessId);

	UFUNCTION(BlueprintCallable, Category = "ControlExternalFiles")
		void TerminateExternalProgram(const int& ProcessId, bool& IsRunning);

	UFUNCTION(BlueprintCallable, Category = "ControlExternalFiles")
		void GetExecutableName(const FString& FileName, bool& result);
};

 

↓[소스파일.cpp]

 

// Fill out your copyright notice in the Description page of Project Settings.


#include "RunExternalTest.h"
// Sets default values
ARunExternalTest::ARunExternalTest()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

}

// Called when the game starts or when spawned
void ARunExternalTest::BeginPlay()
{
	Super::BeginPlay();
}

// Called every frame
void ARunExternalTest::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

/// <summary>
/// 외부파일 실행
/// </summary>
void ARunExternalTest::RunExternalExeFile(const FString& AppPath, bool& Result, int& ProcessId)
{
	//프로세스ID
	uint32 OutProcessId = 0;
	ProcessId = 0;

	Result = false;

	//파일실행
	FProcHandle Proc = FPlatformProcess::CreateProc(*AppPath, NULL, true, false, false, &OutProcessId, -1, nullptr, nullptr);
	
	if (Proc.IsValid())
	{
		//실행되었을경우 Result에 True 리턴
		ProcessId = OutProcessId;
		Result = true;
	}
	else {
		//만약 실행되지않았을경우, 관리자의 권한으로 재실행
		Result = FPlatformProcess::ExecElevatedProcess(*AppPath, NULL, nullptr);
	}
}

/// <summary>
/// 실행중인 외부파일 닫기
/// </summary>
void ARunExternalTest::TerminateExternalProgram(const int& ProcessId, bool& IsRunning)
{
	//Uint로 ProcHandle을 받아옴
	uint32 ProcessIdUint = ProcessId;
	FProcHandle Proc = FPlatformProcess::OpenProcess(ProcessIdUint);

	//실행중인지 확인
	IsRunning = FPlatformProcess::IsProcRunning(Proc);
	if (IsRunning)
	{
		//실행중이라면 닫는다
		FPlatformProcess::TerminateProc(Proc);
		FPlatformProcess::CloseProc(Proc);
	}
}

/// <summary>
/// 파일 실행중 확인
/// </summary>
void ARunExternalTest::GetExecutableName(const FString& FileName, bool& result)
{
	FString Name = FileName;
	result = FPlatformProcess::IsApplicationRunning(*Name);
}

 

 


 

 

 

 

 

최종테스트. 실행과 종료가 잘 되는것을 확인할 수 있다 :)

'Unreal' 카테고리의 다른 글

Unreal Engine 으로 NDI 영상보내기/받기  (0) 2023.11.03

2021.05.07 네이버 블로그 글을 이전하여 포스트 한 게시글입니다.


1) 언리얼 엔진에 NDI 플러그인 설치

아래 링크에 가면 언리얼엔진용 플러그인을 받을 수 있다.

https://www.ndi.tv/sdk/

 

NDI SDK (Software Developer Kit)

Download the royalty free NDI (Network Device Interface) Software Developer Kit to access the tools and resources to integrate native NDI support into your systems, devices, and applications.

www.ndi.tv

 


2) Unreal + NDI 송출

 

NDI송출용으로 만든 프로젝트 세팅이다.

- C++과 블루프린트는 상관없는 듯. 원하는 프로젝트의 방향에 따라 설정하면 됨

 

 

[세팅] -> [플러그인] -> [NDI IO Plugin]으로 플러그인 설정을 해준다

 

 

플러그인 설정이 끝나면 오른쪽 액티비티창에 NDI가 나타난다.

 

NDI broadCast Actor를 레벨에 배치

 

 

컴포넌트의 Media Source가 비어있을것이다.

송출용 Sender 미디어 소스를 만들어준다.

 

 

만들어진 미디어 소스를 더블클릭하면 미디어 소스의 세세한 설정을 수정 할 수 있다.

 

 

 

랜더링 타겟이 없으므로 NDI 송출할 랜더링 타겟을 설정해준다.

 

 

이제 송출용 Actor 블루프린트를 생성한다

 

 

액터 블루프린트에 NDI BroadCast Component 컴포넌트를 추가해준다.

 

 

NDI BroadCast Component 컴포넌트를 추가하면 start Broadcasting 노드로 시작이 가능하다.

 

 

간단한 송출용 블루프린트.

 

 

컴포넌트에 미디어소스가 연결되어있지않으므로 연결해준다.

 

송출 확인 영상은 아래 블로그에서 가능하다.

https://blog.naver.com/keywy0214/222341298926

 


2) Unreal + NDI 수신

송출을 했으니 이번엔 받는 리시버를 만들어보자

 

 

수신용 프로젝트 세팅.

원하는 프로젝트의 방향따라 만들면 된다

 

플러그인을 설정해주고,

 

콘텐츠 브라우저에 NDI Media Receiver를 생성해준다

[미디어] -> [NDI Media Receiver]에 존재

 

 

생성된 미디어 리시버를 더블클릭하여 미디어 텍스쳐를 만들어준다.

 

수신한 화면을 보여줄 곳을 만든다.

저의 경우, Plane을 생성했습니다

 

 

만들어진 Plane에 MediaTexture를 입힌다

 

그 후, 수신전용 액터 블루프린트를 만든다

 

NDI Receiver Component 컴포넌트를 추가한 후,

 

 

간단하게 수신용 블루프린트를 짜보았다

 

이번에도 미디어 소스를 컴포넌트에 연결해주면 완성!

'Unreal' 카테고리의 다른 글

UnrealEngine(UE4)로 외부 프로그램 (.exe) 실행/종료  (0) 2023.11.03

+ Recent posts