유니티3D - 10
이번 목표
Player 상태창(HP, 총알) UI 만들기 , 총알 수 만들기 , 상점에서 총기 구매 프로젝트 디테일 마무리 등
상태창 만들기
일단 UI 클래스를 만들어서 관리하겠다. 이 UI도 싱글톤처럼 이용해보자. 슬라이더와 텍스트메쉬를 이용해서 구현할것이고, Player에서 weapon,Status 컴포넌트를 처음에 캐싱해와서 이용할 것
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class UIPlayerHP : MonoBehaviour
{
public static UIPlayerHP Instance;
[SerializeField] private Slider Slider;
[SerializeField] private TextMeshProUGUI BulletText;
private WeaponComponent weapon;
private Status Status;
public void Init(WeaponComponent Inweapon, Status InStauts)
{
weapon = Inweapon;
Status = InStauts;
SetHP();
}
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
}
void Start()
{
}
void SetHP()
{
float CurrentHP = Status.GetCurrentHP();
float MaxHP = Status.GetMaxHP();
float PercentHP = CurrentHP / MaxHP;
Slider.value = PercentHP;
}
void Update()
{
}
}
일단 이렇게 코드를 만들어 놓았고 이렇게 만들었으면 일단 HP바를 업데이트 하려면 HP가 변경될 때 마다 호출되어야한다.
하지만 Status에서 UI로 바로 인스턴스를 받아서 하면 AI도 이 컴포넌트를 사용하기 떄문에 델리게이트를 이용해서 Player에 바인딩하여 SetHP함수를 호출하도록 하겠다.
만들어보고 구조를 보아하니 weapon과 Status를 UI쪽에서 캐싱할 필요는 없던거같다.
그냥 델리게이트의 파라미터에 필요한 데이터를 가져오는게 맞았던거 같다.
다시 코드를 만들어보자 일단 Status에서 델리게이트로 넘기는 코드부터 구현해보자.
HP바 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using System;
using UnityEngine;
public class Status : MonoBehaviour
{
[SerializeField] private float maxHP = 100f;
[SerializeField] private float currentHP;
private State State;
private IController controller;
public event Action<float, float> OnHPChanged;
void Start()
{
currentHP = maxHP;
State = GetComponent<State>();
controller = GetComponent<IController>();
}
public void IncreaseHP(float amount)
{
currentHP = Mathf.Clamp(currentHP + amount, 0, maxHP);
OnHPChanged.Invoke(currentHP , maxHP);
}
public void DecreaseHP(float amount)
{
if (currentHP == 0) return;
currentHP = Mathf.Clamp(currentHP - amount, 0, maxHP);
OnHPChanged.Invoke(currentHP, maxHP);
if (currentHP <= 0)
{
OnDead();
}
}
public float GetCurrentHP() => currentHP;
public float GetMaxHP() => maxHP;
public float GetHPPercent() => currentHP / maxHP;
private void OnDead()
{
State.OnDead();
controller.OnDead();
}
}
일단 HP가 변경할 때 두 경우 둘다 델리게이트를 호출해준다.
그리고 Player가 Status를 가지고있으니 UI 인스턴스를 사용해 HPUI를 바꿔주겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void OnEnable()
{
Status.OnHPChanged += OnHPChanged;
}
void OnDisable()
{
Status.OnHPChanged -= OnHPChanged;
}
void OnHPChanged(float InCurrentHP, float InMaxHP)
{
float HPPercent = InCurrentHP / InMaxHP;
UIPlayerHP.Instance.SetHP(HPPercent);
}
이제 확인해보면 될 거 같다.
HPUI반영
잘 된다 이제 현재 Gun을 가져와서 총알 수도 반영해보자.
총알 수 구현
Gun 클래스 부모인 Waeapon 클래스에 UIUpdate 가상함수를 만들어 무기마다 다르게 적용할 수 있게 만들고
Gun 클래스에선 호출될 때 Waepon 클래스의 델리게이트를 이용해 int 변수에 Bullet을 넘겨 사용한다.
Player만 되야하기때문에 WeaponComponet에서도 델리게이트를 이용해 플레이어 함수에 바인딩하는 구조로 설계했다.
Gun -> WeaponComp -> Player 순으로 델리게이트가 호출되는 구조이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Weapon
{
public Action<int> OnCurrentBulletChanged;
public virtual void UIUpdate() { }
}
class GunWeapon : Weapon
{
public override void UIUpdate()
{
OnCurrentBulletChanged.Invoke(bullet);
}
}
class WeaponComponent
{
public event Action<int> OnCurrentAmountEvent;
CurrentWeapon.OnCurrentBulletChanged += OnCurrentBulletChanged;
public void OnCurrentBulletChanged(int Current)
{
OnCurrentAmountEvent.Invoke(Current);
}
}
class Player
{
weapon.OnCurrentAmountEvent += UICurrentUpdate;
void UICurrentUpdate(int Current)
{
UIPlayerHP.Instance.UpdateCurrent(Current);
}
}
이런식으로 UI에 현재 무기의 정보를 넘겨주어 처리하였다.
비슷한 방식으로 WeaponComponent에서 여분총알을 가지고있기 떄문에 바로 WeaponComp ->Player 를 사용한 델리게이트로 여분의총알도 처리하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class WeaponComponent
{
public event Action<int> OnWaitingAmountEvent;
void Start()
{
OnWaitingAmountEvent.Invoke(Bullet);
}
}
class Player
{
weapon.OnWaitingAmountEvent += UIWaitingUpdate;
void UIWaitingUpdate(int Current)
{
UIPlayerHP.Instance.UpdateWaiting(Current);
}
}
이제 실행을 해보자
총알UI
영상으로 보면 0발일때 처리를 안해서 음수까지 넘어가는 모습이다.
fire부분에서 예외처리를 해주도록 하고 재장전을 구현해보자.
1
2
3
4
5
6
7
8
public virtual void fire()
{
if (bullet <= 0)
{
OnReLoading();
return;
}
}
일단 이렇게만 구현해도 음수까지는 넘어가지지 않는다. 그리고 재장전을 할 때 애니메이션도 호출이되야하므로 델리게이트를 활용하자
나의 AnimController가 WeaponComponent를 캐싱하고있기 때문에 전의 UI와 비슷하게
Gun -> WeaponComp -> Animcontroller 구조로 델리게이트를 연속 호출한다.
하지만 여기서 GunWeapon 클래스만 저 델리게이트변수 (이벤트변수)를 소유하고있기때문에 WeaponComp에서는 Weapon 클래스만 캐싱하게끔 설계했다.
여기서 이제 현재 무기를 바꿀때마다 현재 무기에 GunWeapon 컴포넌트가 맞는지 확인하여 따로 이 타이밍에 바인딩을 하도록 하겠다.
지금 무기를 바꾸는 코드는 없다 여기서 나는 상점을 만들면서 무기를 추가하고 무기를 바꾸는 코드를 둘 다 구현하여 재장전까지 설계하겠다.
일단 Market 클래스를 만들어 필요한 정보들을 생각해보자.
일단 아이템의 Prefab , 이미지 , 가격 이 3개가 있어야한다.
이 것들을 ItemData라는 클래스의 스크립트테이블오브젝트로 관리하겠다.
1
2
3
4
5
6
7
8
9
10
using UnityEngine;
[CreateAssetMenu(fileName = "MarketItemData", menuName = "Scriptable Objects/MarketItemData")]
public class ItemData : ScriptableObject
{
public GameObject ItemPrefab;
public Sprite Image;
public int Price;
}
그리고 UI의 버튼을 이용해서 아이템을 구매를 할 것이니 버튼 스크립트 UIItemButton을 생성한다.
이 버튼 스크립트에서는 가격과 이미지를 ItemData에서 받아와 게임이 시작하면 알맞게 들어가게 하도록하겠다.
- 현재 UI 구조
1 2 3 4 5 6 7
Market ㄴUIItemButton ㄴUIItemButton2 ㄴUIItemButton3 ㄴUIItemButton4 ㄴUIItemButton5 ㄴUIItemButton6
이런식으로 있기 때문에 버튼클래스에서 Market변수를 인스펙터창에서 캐싱해주겠다.
Market 클래스에 Itemdas[] 변수를 소유하고있다.1 2 3 4
public class Market : MonoBehaviour { public ItemData[] ItemDatas; }
여기서 알맞는 데이터를 가져오기위해 버튼클래스에 Index변수로 구분하여 가져오겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class UIItemButton : MonoBehaviour
{
public TextMeshProUGUI Price;
public Image ItemImage;
public int Index;
public Market Market;
private ItemData ItemData;
void Start()
{
ItemData = Market.GetItemData(Index);
SetPriceText();
SetItemImage();
}
public void SetPriceText()
{
Price.text = "$ " + ItemData.Price;
}
public void SetItemImage()
{
ItemImage.sprite = ItemData.Image;
}
}
상점이미지
알맞게 잘 들어간다. 이제 버튼을 클릭하여 무기를 장착할 수 있게 해보자.
일단 무기를 먼저 얻고 코인을 감소시켜보자.
첫째로 아이템버튼을 눌렀을 때 어떤 Index를 클릭했는지 Market에다 정보를 넘긴다.
그리고 이제 PlayerInfo 클래스를 만들어 얼마를 소유하고 있는지 알아보도록한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using UnityEngine;
public class PlayerInfo : MonoBehaviour
{
public static PlayerInfo Instance;
private int Gold;
[SerializeField]
private PlayerController Player;
public int GetGold() { return Gold; }
public void InCreaseGold(int InGold) { Gold += InGold; }
public void DecreaseGold(int DecreaseGold) { Gold -= DecreaseGold; }
public PlayerController GetPlayer() { return Player; }
void Start()
{
Gold = 150;
}
private void Awake()
{
Instance = this;
}
}
코드처럼 간단한 골드 감소, 증가를 구현한 모습이다.
여기에다가 총알 정보등을 넣고 Player에서 델리게이트를 이용해 바로 UI로 넘어갔는데 여기서 처리해주는게 더 깔끔 할 거 같다.
일단 Gold를 이용한 거래를 만들고 수정해보도록 하겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using Unity.AppUI.UI;
using UnityEngine;
using UnityEngine.UI;
public class Market : MonoBehaviour
{
public ItemData[] ItemDatas;
int CurrentIndex = -1;
private PlayerInfo playerInfo;
void Start()
{
playerInfo = PlayerInfo.Instance;
}
public ItemData GetItemData(int index) { return ItemDatas[index]; }
bool IsCanBuyItem()
{
if(ItemDatas[CurrentIndex].Price <= playerInfo.GetGold())
{
return true;
}
else
return false;
}
public void BuyItem(int Index)
{
CurrentIndex = Index;
if(IsCanBuyItem())
{
playerInfo.DecreaseGold(ItemDatas[CurrentIndex].Price);
}
}
public void ShowMarket()
{
gameObject.SetActive(true);
Cursor.lockState = CursorLockMode.Confined;
Cursor.visible = true;
}
public void HideMarket()
{
gameObject.SetActive(false);
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
}
코드처럼 PalyerInfo에서 Item구매가 가능한지 확인 후 골드를 사용하도록 만들었다.
이제 Player에게 Item을 넘겨보도록하자 그러기 이때를 위해 PlayerInfo에 Player를 캐싱해두었다.
Player에서 내가 Drop된 아이템을 픽업하는 코드를 만들었는데 다시보니 정말 범용성 없게 만들었다고 생각이든다.
근데 픽업과 아이템 구매는 결이 다르기 때문에 코드 재활용은 힘들거같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Palyer
{
public void BuyItem(Weapon Inweapon)
{
weapon.BuyItemDrop(Inweapon);
}
}
class WeaponComponent
{
public void BuyItemDrop(Weapon Item)
{
switch (Item.GetWeaponType())
{
case EWeaponType.Rifle:
InputMainGun(Item);
break;
case EWeaponType.Pistol:
InputPistol(Item);
break;
case EWeaponType.Throwing:
InputThrowing(Item);
break;
}
}
}
Player를 통해 WeaponComponet에 전달하여 구현했고 무기타입에 맞게 스위치문을 활용해 처리해주었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
public void InputMainGun(Weapon Item)
{
if (MainGun != null)
{
Destroy(MainGun);
}
MainGun = Instantiate(Item);
MainGun.SetOwner(gameObject);
GunWeapon Gun = MainGun.GetComponent<GunWeapon>();
Gun.ReLoadingEvent += OnReLoadingEvent;
MainGun.OnCurrentBulletChanged += OnCurrentBulletChanged;
MainGun.gameObject.SetActive(false);
}
같은타입의 무기를 가지고있다면 무기를 삭제 후 다시 생성 후 캐싱하였다.
그 과정에서 재장전 델리게이트도 미리 바인딩해주었다.
현재무기 관리를 SetActive() 로 On/Off 하여 관리해주었다.
무기를 바꿀때 int값을 받아 구분하여 무기를 정한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void ChangeWeapon(int Index)
{
if (CurrentWeapon != null)
CurrentWeapon.gameObject.SetActive(false);
switch (Index)
{
case 1:
CurrentWeapon = MainGun;
break;
case 2:
CurrentWeapon = pistol;
break;
case 3:
CurrentWeapon = Throw;
break;
default:
CurrentWeapon = null;
break;
}
CurrentWeapon.gameObject.SetActive(true);
CurrentWeapon.UIUpdate();
}
무기구매영상1
무기가 잘 구매되고 1번키를 눌렀을 때 활성화가되고 총도 잘쏴진다.
하지만 지금 총을 쏘지않아도 계속 쏘는 모션이 나간다.
일단 장전을 구현해보자 총을 쏘기전에 OnFire() 부분에 총알을 확인하고 장전하고 그리고 총을 쏘기 시작한순간 Fire() 함수에도 총알을 확인해준다.
장전 델리게이트를 만들어 WeapoComp에서 AnimContoller로 또 델레게이트를 이용해 재장전 모션을 실행시킨다.
그리고 애니메이션 이벤트를 이용해 장전모션이 거의 끝날때 총알을 충전시키도록 하겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public override void OnFire()
{
if (bullet <= 0)
{
OnReLoading();
return;
}
if (firing == true) return;
firing = true;
if (data.auto)
{
StartCoroutine("FireLoop");
}
else
{
fire();
}
}
public virtual void fire()
{
if (bullet <= 0)
{
OnReLoading();
firing = false;
StopCoroutine("FireLoop");
return;
}
}
private void OnReLoading()
{
ReLoadingEvent.Invoke();
}
장전 조건과 애니메이션 재생 후 이제 장전을 할때 여분총알과 얼만큼 넣어야하는지 계산해서 넣어주고 UI도 업데이트해줘야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void ReLoading()
{
GunWeapon Gun = CurrentWeapon.GetComponent<GunWeapon>();
int max = Gun.data.maxBullet;
int current = Gun.GetCurrentBullet();
int need = Mathf.Clamp(max - current, 0, max);
int amountToReload = Mathf.Min(need, Bullet);
if (amountToReload <= 0)
return;
Bullet -= amountToReload;
Gun.ReLoading(current + amountToReload);
OnWaitingAmountEvent.Invoke(Bullet);
}
장전실패
장전 애니메이션은 잘 실행되는데 실제 장전은 되지않는다.
보아하니 애니메이션 이벤트가 실행되는 주체에 함수를 찾아서 실행시킨다.
1
2
Player
ㄴ CharacterMesh
이 구조이기 때문에 Player에 있는 WeaponComp에 장전함수가 호출되지않는다.
AnimController에서 델리게이트를 만들어 처리하는 방법이 깔끔한거같다.
장전성공
잘 되는 모습이다 하지만 여유 탄창이 없는경우를 예외처리 해주도록하면 완벽할 거 같다.
무기스왑
주무기와 권총을 스왑할 때 UI변경과 현재 무기 변경을 알맞도록 바꾸고 상점에 있는 골드 정보와 총정보도 업데이트해주도록하자
WeaponComp에 총 장착 해제 할때 델리게이트를 이용해 애님컨트롤러에 전달하는 방식으로 똑같이했다.
애님컨트롤러에서 호출할 때 레이어를 UpperBody로 바꾸어주고 애님이벤트로 다시 바꿔주었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void OnGrabGun()
{
animator.SetTrigger("GrabGun");
targetWeight = 1f;
isBlending = true;
}
void OnPutGun()
{
animator.SetTrigger("PutGun");
targetWeight = 1f;
isBlending = true;
}
public void EndLayerUpper()
{
targetWeight = 0f;
isBlending = true;
}
무기스왑버그
좀 어색하지만 나쁘지는 않다 하지만 문제는 총을 꺼낼때마다 총소리가나는데 아마 총을 집어넣는 조건을 안만들어서 그런거같다.
계속 CurrentWeapon.gameObject.SetActive(true); 호출되어 GunWeapon의 Start() 가 실행되어 오디오 설정 부분이 나오는거 같다.
찾아보니까 Audio Source에 활성 상태일 때 재생을 true로 했는데 컴포넌트가 활성될때 재생되는거였다.
그 다음 문제는 애니메이션인데 아직 유니티의 애니메이터 구조를 내가 정확히 파악하지못한거같다.
AnyState에서 실행되 애니메이션도 다시 기본 애니메이션으로 돌아가게끔 트렌지션을 만들어줘야한다.
언리얼의 몽타주처럼 한 번 실행된후에 다시 원래 애니메이션으로 돌아가는 구조인줄알았는데 아니였다.
그냥 어느상황에서든 이 애니메이션이 실행될 수 있다는 것 이였다. 그리고 트리거로 재생된 애니메이션은 돌아가는 트렌지션만 만들어놓아도 알아서 돌아간다.
인스펙터창에 종료 시간 있음 변수를 true로 하면 종료시간이 있어서 그 시간만에 바로 바뀔줄알고했는데 오히려 더 딜레이가 걸리고 0초로해도 더 이상해졌다.
오히려 애니메이션을 안끊고 넘어가는 역할이여서 그런 거 같다.
스왑
전보다 훨씬 자연스러운 애니메이션 전환이다.
이제 상점UI에서 Player총기 목록을 업데이트 해주도록하자 간단하게 델리게이트를 이용해 무기를 살 때와 상점을 열을 때 실행하면될거같다.
- Market 코드
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
public void UpdatePlayerInfo() { UpdatePlayerWeapon(MainGun, playerInfo.MaingGun.WeaponImage); UpdatePlayerWeapon(Pistol, playerInfo.Pistol.WeaponImage); } private void UpdatePlayerWeapon(Image SetUpImage, Sprite WeaponImage) { if (WeaponImage != null) { SetUpImage.sprite = WeaponImage; SetUpImage.color = new Color32(255, 255, 255, 255); } else { SetUpImage.sprite = null; SetUpImage.color = new Color32(94, 94, 94, 203); } }
구매시 문제영상
이렇게하면 문제점이 생긴다 바로 메인건이 없기때문에 밑에 코드로 가지않는다 Null체크를 해야겠다.
1
2
3
4
5
6
7
public void UpdatePlayerInfo()
{
if(playerInfo.MaingGun)
UpdatePlayerWeapon(MainGun, playerInfo.MaingGun.WeaponImage);
if (playerInfo.Pistol)
UpdatePlayerWeapon(Pistol, playerInfo.Pistol.WeaponImage);
}
구매시 해결영상
전반적인 상점에서 구매시스템과 무기 스왑등을 마무리했다.
