2022. 10. 5. 20:14ㆍProjects/Charon
로그라이크 게임이니, 무기를 사용하여 몬스터들을 때려잡을 수 있어야 할 겁니다. 그만큼 무기라는 것이 우리 게임에서 핵심이 된다는 얘기죠. 우선 우리 게임의 기본 무기인 "카론의 노(Charon's Paddle)"로 기능을 구현해볼 겁니다. 하지만 이번 글에서는 그 기반만 먼저 다져볼까 해요!
솔직히 말해서 무기가 노처럼 생기진 않았습니다. 구매했던 무기 에셋 중에 그나마 노 같이 생긴 걸로 쥐어 주었어요...
그리고 게임 그래픽이 너무 밋밋한 것 같아서 URP(Universal Render Pipeline)으로 프로젝트를 업그레이드 하고 싶었는데, 커스텀 쉐이더(Custom shader)는 URP Shader로 자동 변환이 안 되더라구요.
현재 구입했던 에셋 중에 물과 검기 이펙트 에셋이 커스템 쉐이더를 사용하고 있습니다. 쉐이더에 대해 공부해 본 적이 아직 없기 때문에 쉐이더 재작성은 무리라 생각하여 지금 그대로 진행하기로 결정했습니다... 다음 게임은 언리얼 엔진(Unreal Engine)으로 만들어 봐야 겠어요...
그럼 이번 주에 제가 무기 시스템을 구현하기 위해 어떤 기반을 먼저 다졌는지, 어떠한 문제들을 겪었는지 설명을 드리도록 할게요!
1. 공격 애니메이션 수정하기
휴머노이드 애니메이션(Humanoid Animation)의 문제점
카론의 노 기본 공격은 총 3가지 콤보로 구성되어 있습니다. 일정 시간 내에 공격 커맨드를 입력하면 다음 콤보로 이어가고, 아니라면 처음 공격으로 초기화 되는 방식이죠.
*애니메이션을 잘못 건드렸는진 모르겠는데, 실행하면 자꾸 희한한 곳으로 위치 이동하여 재생하더라구요... 그래도 인게임에서는 정상 작동합니다.
여기에 이제 손돌의 손에다 무기를 쥐어주고, 실행만 하면 모든 게 잘 될 것처럼 보입니다. 하지만 인생은 그렇게 쉬운 것이 아니죠...
아주 난리가 난 모습을 볼 수 있습니다. 애니메이션 키 프레임을 수정하는 작업이 필요할 것 같네요.
하지만 휴머노이드 애니메이션은 Unity 내에서 키 프레임 수정하는 것이 불가능합니다. 해당 애니메이션을 만들었던 외부 프로그램에서 수정하여 다시 Unity로 임포트 해야 한다고 해요.
그러면 수정할 수 있는 부분을 새로 만들어 주자!
애니메이션을 만들 당시에 사용되었던 손돌의 신체들을 수정하지 못하는 것이지, 새로 추가한 것은 수정하지 못한다고는 안 했습니다. 저는 다음과 같은 과정을 거쳤죠.
- 빈 오브젝트(HandleWeaponPosition)을 생성하여, 손돌의 손(RightHand)과 나란히 둔다.
- 손돌의 손(RightHand) 트랜스폼 값을 빈 오브젝트(HandleWeaponPosition)의 트랜스폼에 복붙한다.
- 즉, 새로 생성한 빈 오브젝트가 무기를 쥐는 손돌의 손 역할을 해줄 겁니다.
- 앞으로 각 무기들은 이 빈 오브젝트(HandleWeaponPosition) 자식 오브젝트로 배치한다.
새로 만든 저 빈 오브젝트(HandleWeaponPosition)으로 키 프레임을 추가하여 각 애니메이션의 무기 배치를 수정해주도록 합시다. 각 애니메이션 키 프레임마다 무기가 이상한 각도로 들려 있거나, 이상한 곳에 있으면 수정해주면 돼요!
애니메이션에 대한 수정 작업은 끝났네요! 이제 무기 탈장착 시스템을 만들어 보죠.
2. 무기 탈장착 시스템 설계하기
게임의 규모에 따라 무기 종류는 굉장히 많을 수도, 적을 수도 있습니다. 메이플스토리 같은 경우만 보더라도 무기가 굉장히 많은 것을 볼 수 있죠. 게임 내에 있는 모든 무기 종류를 읽어 메모리에 올리는 것도 뭔가 낭비인 느낌입니다.
그래서 제가 생각한 것은 유저의 사용 가능한 무기 목록들만 가져와서 올리자는 것입니다. 우리 게임에서는 실시간으로 무기 교체는 하지 못 하고, 로비에서만 무기 교체가 가능합니다. 처음 플레이 할 때는 카론의 노 밖에 사용하지 못하겠지만, 플레이 하며 시간이 지나 다른 무기 사용 조건들을 충족한다면 사용할 수 있는 무기 종류들이 늘어 가겠죠.
DB나 기타 저장소에는 해당 유저가 사용 가능한 무기의 파일 경로를 가지고 있어야 할 겁니다. 그리고 특정 상황이 되면 해당 무기를 메모리로 읽어오는 것이죠. 제가 생각하는 특정 상황이라 함은 다음과 같습니다.
- 게임을 처음 실행하는 초기화 단계에서 유저가 사용 가능했던 무기 목록들을 읽어올 때
- 게임 중, 새로운 무기를 사용할 수 있게 되었을 때
물론 저장/불러오기 시스템을 제가 담당한 것이 아니기에, 해당 파트를 맡은 다른 팀원과도 상의를 했지요. 이걸 토대로 우선 임시 테스트용을 만들어 볼 겁니다. 하나씩 제가 했던 순서대로 설명하겠습니다.
Scriptable Object로 무기를 쥐는 로컬 좌표 정보 저장하기
무기 종류가 여러 가지일 수는 있지만, 캐릭터가 모든 무기를 쥐는 방법이 전부 다르지는 않을 겁니다. 여러 무기들이 있더라도 그 무기들이 한손 무기 타입이면, 캐릭터는 해당 무기들을 쥘 때 한 손으로만 쥘 겁니다.
즉, 쥐는 방법이 공통적인 무기들도 많다는 것입니다. 이러한 점을 미루어 보았을 때, 스크립터블 오브젝트(Scriptable Object)로 관리하면 굉장히 편하겠다는 생각이 들었습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "Weapon Handle Data", menuName = "Scriptable Object/Weapon Handle Data", order = int.MaxValue)]
public class WeaponHandleData : ScriptableObject
{
public Vector3 localPosition;
public Vector3 localRotation;
public Vector3 localScale;
}
무기는 손의 자식 오브젝트로 배치되기 때문에, 월드 좌표가 아닌 로컬 좌표 정보를 가지고 있어야 합니다. 이걸 토대로 카론의 노를 쥘 때의 로컬 좌표 정보를 저장하였습니다.
실제로 캐릭터에 쥐어보게 한 후에 트랜스폼(Transform)을 보니, 위와 같더군요. 그래서 해당 정보를 복사하여 붙여넣어 줬습니다. 이제 실제 무기 클래스를 만들어 봅시다.
무기 클래스와 무기를 관리하는 클래스 정의하기
모든 무기들의 공통적인 부분들을 모아서, 추상 클래스로 만들어 주었습니다.
using CharacterController;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class BaseWeapon : MonoBehaviour
{
// 무기의 현재 콤보 카운트
public int ComboCount { get; set; }
// 이 무기를 쥘 때의 로컬 좌표 정보
public WeaponHandleData HandleData { get { return weaponhandleData; } }
// 이 무기를 사용할 때의 애니메이터
public RuntimeAnimatorController WeaponAnimator { get { return weaponAnimator; } }
public string Name { get { return _name; } }
public float AttackDamage { get { return attackDamage; } }
public float AttackSpeed { get { return attackSpeed; } }
public float AttackRange { get { return attackRange; } }
#region #무기 정보
[Header("생성 정보"), Tooltip("해당 무기를 쥐었을 때의 Local Transform 값 정보입니다.")]
[SerializeField] protected WeaponHandleData weaponhandleData;
[Header("무기 정보")]
[SerializeField] protected RuntimeAnimatorController weaponAnimator;
[SerializeField] protected string _name;
[SerializeField] protected float attackDamage;
[SerializeField] protected float attackSpeed;
[SerializeField] protected float attackRange;
#endregion
public void SetWeaponData(string name, float attackDamage, float attackSpeed, float attackRange)
{
this._name = name;
this.attackDamage = attackDamage;
this.attackSpeed = attackSpeed;
this.attackRange = attackRange;
}
public abstract void Attack(BaseState state); // 기본 공격
public abstract void DashAttack(BaseState state); // 대시 공격
public abstract void ChargingAttack(BaseState state); // 차지 공격
public abstract void Skill(BaseState state); // 스킬
public abstract void UltimateSkill(BaseState state); // 궁극기
}
그렇다면, 이러한 무기들을 관리해주는 클래스도 필요할 겁니다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WeaponManager
{
// 현재 무기 스크립트
public BaseWeapon Weapon { get; private set; }
// 이 부분은 크게 신경 안 쓰셔도 됩니다.
public Action<GameObject> unRegisterWeapon { get; set; }
// 무기를 쥐는 손의 트랜스폼
private Transform handPosition;
// 현재 내 무기 오브젝트
private GameObject weaponObject;
// 현재 WeaponManager에 등록된 무기 리스트
private List<GameObject> weapons = new List<GameObject>();
public WeaponManager(Transform hand)
{
handPosition = hand;
}
// 무기 등록
public void RegisterWeapon(GameObject weapon)
{
if (!weapons.Contains(weapon))
{
BaseWeapon weaponInfo = weapon.GetComponent<BaseWeapon>();
weapon.transform.SetParent(handPosition);
weapon.transform.localPosition = weaponInfo.HandleData.localPosition;
weapon.transform.localEulerAngles = weaponInfo.HandleData.localRotation;
weapon.transform.localScale = weaponInfo.HandleData.localScale;
weapons.Add(weapon);
weapon.SetActive(false);
}
}
// 무기 삭제
public void UnRegisterWeapon(GameObject weapon)
{
if (weapons.Contains(weapon))
{
weapons.Remove(weapon);
unRegisterWeapon.Invoke(weapon);
}
}
// 무기 변경
public void SetWeapon(GameObject weapon)
{
if (Weapon == null)
{
weaponObject = weapon;
Weapon = weapon.GetComponent<BaseWeapon>();
weaponObject.SetActive(true);
Player.Instance.animator.runtimeAnimatorController = Weapon.WeaponAnimator;
return;
}
for(int i = 0; i < weapons.Count; i++)
{
if (weapons[i].Equals(Weapon))
{
weaponObject = weapon;
weaponObject.SetActive(true);
Weapon = weapon.GetComponent<BaseWeapon>();
Player.Instance.animator.runtimeAnimatorController = Weapon.WeaponAnimator;
continue;
}
weapons[i].SetActive(false);
}
}
}
코드가 그렇게 어렵진 않을 거라 생각합니다. 현재 내가 사용하는 무기만 활성화 되어있고, 나머지 무기들은 비활성화된 채로 쥐고 있다는 것이 핵심이 되겠네요. WeaponManager 클래스를 만들었으니, Player 스크립트에 선언해주도록 합시다.
이제 임시로 한 번 테스트해보도록 하죠. 빈 오브젝트에 인벤토리 매니저 오브젝트를 만들어서, 카론의 노 무기를 등록하여 봅시다.
*새로운 무기를 사용 가능할 때에 갱신되는 코드 부분은 작성하지 않았습니다. 나중에 진행이 더 되면 그때 만들려구요!
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class InventoryManager : MonoBehaviour
{
public static InventoryManager Instance { get; private set; }
private static InventoryManager instance;
public GameObject charonPaddle; /// 테스트용 카론의 노
void Awake()
{
if(instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
return;
}
DestroyImmediate(gameObject);
}
void Start()
{
Init();
}
private void Init() /// 게임 시작 후 유저의 사용 가능한 무기를 읽어오는 초기화 메소드
{
/*
---------슈도 코드---------
대강 이런 로직으로 돌아간다고만 생각을 해두었습니다.
GameObject[] weapons = Database.LoadWeapons();
for(int i = 0; i < weapons.Length; i++)
{
Player.Instance.weaponManager.RegisterWeapon(weapon[i]);
}
*/
// 카론의 노 테스트용
GameObject weapon = Instantiate(charonPaddle);
Player.Instance.weaponManager.RegisterWeapon(weapon);
Player.Instance.weaponManager.SetWeapon(weapon);
}
}
이제 게임을 실행해보면, 원래 빈손이었지만 카론의 노를 쥐고 있는 모습을 볼 수 있을 겁니다.
지금은 무기를 하나 밖에 추가하지 않았지만 사용 가능한 무기들이 여러 개였다면, HandleWeaponPosition 자식으로 생성되었을 것이고, 현재 사용 중인 무기만 활성화가 되어 있겠지요. 이런 기반을 만들어 놓았으니, 앞으로 새로운 무기를 만들게 되면, 여기에 등록하기만 하면 되겠습니다.
더 작성하려고 했는데, 글이 길어질 것 같아서 다음 글에서 써야할 것 같네요. 다음 글에서는 다음 내용들을 다뤄볼까 합니다.
- 사용자의 여러 공격 키 입력들 받아 분기 처리하기
- 공격 상태(AttackState)를 상태머신(StateMachine)에 추가하기
- 카론의 노 기본 3타 공격 구현하기
긴 글 읽어주셔서 감사합니다!
'Projects > Charon' 카테고리의 다른 글
[Charon] #8. 플레이어와 카메라 사이의 오브젝트 투명화하기 (6) | 2022.11.16 |
---|---|
[Charon] #7. 무기 기본 3타 콤보 공격 구현하기 (긴 글 주의) (0) | 2022.10.06 |
[Charon] #5. 상태 패턴(State Pattern) 도입하기 (4) | 2022.10.04 |
[Charon] #4. RigidBody를 이용한 3D 캐릭터 N단 대시(Dash) 구현하기 (4) | 2022.09.21 |
[Charon] #3. RigidBody를 사용한 3D 캐릭터의 경사로(Slope) 지형 이동 구현하기 (9) | 2022.08.30 |