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에서 입력을 담당한다.
현재의 기능으로 설명하면,
버튼을 누르는 Action이 Controller로 들어온다.
Controller에서 Model에게 HP값 변경을 요청한다.
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가 존재한다.
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이 늘어난다.
현재의 기능으로 설명하면,
값을 받아오는 것 까지는 MVP패턴과 비슷하다.
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;
}
}
유니티쨩 범위 콜라이더안에 들어오면 리스트에 넣고, 범위밖으로 벗어나면 리스트에서 제외한다.
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급이었던 스칼렛 스트링스의 스킬을 직접 도전해보니 나름 즐거운 시간이었던 것 같다.
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;
}
}
저는 편의상 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);
};
}
}
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("애니메이션이 끝났습니다");
}
}
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개로 나눈다.
새로 생긴 점들을 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;
}
}
네트워크로부터 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;
}