캐릭터 스킬 사용 구현
캐릭터 스킬 사용 구현
캐릭터 스킬 버튼을 누르면 스킬을 사용하고, 스킬을 사용하는 동안은 캐릭터가 움직이지 않도록 구현한다.
1. 스킬 버튼 UI 만들기
캔버스 아래에 스킬 버튼 생성
- Unity의 Canvas 안에 스킬 버튼을 추가
- 버튼에 다운로드한 스킬 아이콘 이미지 추가
- CButton Script 컴포넌트 추가
2. CButton 코드
public class CButton : MonoBehaviour
{
[SerializeField]private Button button;
private void Awake() //꼭 Awake로 해주기
{
button = GetComponent<Button>();
}
public void AddListener(UnityAction listener)
{
button.onClick.AddListener(listener);
}
public void RemoveListener(UnityAction listener)
{
button.onClick.RemoveListener(listener);
}
}
CButton 코드 설명
- Awake()에서 Button 컴포넌트를 가져오기
- 버튼을 실행 시 바로 사용할 수 있도록 Awake()에서 가져온다.
- **AddListener**와 **RemoveListener**로 버튼에 클릭 이벤트를 추가, 제거
3. 애니메이션 이벤트 설정 방법
- Player 누른 상태에서 원하는 애니메이션 클립을 열고 이벤트를 추가할 타이밍에 Animation Event를 추가
- 이벤트로 CanMove 메서드를 호출
- 스킬 시작 시 CanMove(0)을 호출.
- 스킬 종료 시 CanMove(1)을 호출.
Material을 URP에 맞는 셰이더와 설정으로 자동 변환
4. 캐릭터 컨트롤러 수정
CharController 코드 수정 부분
public class CharController : MonoBehaviour
{
public List<CButton> _buttons; //스킬 버튼 리스트
void Start()
{
foreach (var CButton in _buttons) //버튼 리스트에 FireSkill 연결
{
CButton.AddListener(FireSkill);
}
}
private bool canMove = true;
void CanMove(int bMove) //이동 가능 여부 설정
{
canMove = bMove == 1;
}
void FireSkill() //스킬 사용
{
_animator.Rebind();
_animator.Play("Attack");
}
void FixedUpdate()
{
Vector2 moveValue = Move_Input.ReadValue<Vector2>(); //이동 입력 값 읽기
if (!canMove) //이동 불가 상태일 때 멈춤
{
moveValue = Vector2.zero;
}
}
}
CharController 코드 설명
- Start(): _buttons에 있는 버튼 리스트를 순회하며 스킬 버튼 클릭 시 FireSkill이 실행되도록 이벤트를 추가
- CanMove(): 애니메이션 이벤트를 통해 호출됨 / bMove 값에 따라 캐릭터 이동 가능 여부를 설정
- bMove == 0: 이동 불가 (스킬 사용 중) bMove == 1: 이동 가능 (스킬 사용 종료).
- FireSkill(): 캐릭터의 애니메이션 상태 초기화 후 공격 애니메이션 재생
- FixedUpdate(): 이동 입력 값을 확인한 후 이동 불가 상태(!canMove)일 때 캐릭터가 멈추도록 설정
Player Inspector창 값 넣기
5. 스킬에 이펙트 추가
DamageField 오브젝트에 원하는 Spell을 자식오브젝트로 넣어준다. (강사님이 사용하신 Spell은 Incinerate Spell)
- DamageField 컴포넌트 추가
- Capsule Collider 2D 컴포넌트 추가
- Layer 변경: DanageField
6. Tag 변경
Player HitCollision의 Tag는 Player로,
Monster HitCollision의 Tag는 Enemy로 변경해준다.
Physics 2D 수정
DamageField 코드
public class DamageField : MonoBehaviour
{
public string MyOwnerTag;
private void OnTriggerEnter2D(Collider2D other)
{
if (!other.CompareTag(MyOwnerTag))
{
}
}
}
CharController 코드 FireDamageField() 수정
void FireDamageField(int index)
{
GameObject go = Instantiate(_damageFields[index].gameObject);
go.GetComponent<DamageField>().MyOwnerTag = "Player";
go.transform.position = transform.position + transform.right * _damageFieldDatas[index].distance;
Destroy(go, 3.0f);
}
결과
애니메이터 확장 툴 제작
애니메이터 확장 툴 제작
: 애니메이터를 확장하는 액션툴을 제작하여 애니메이션에 필요한 이벤트나 데이터를 더 효율적으로 관리할 수 있음.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
public class ActionNodeBase
{
}
public class AnimationNode : ActionNodeBase
{
public string name;
public float startTime;
public float duration;
}
[CustomEditor(typeof(ActionToolEditor))]
public class ActionToolEditor : EditorWindow
{
private List<ActionNodeBase> nodes = new List<ActionNodeBase>();
private ActionNodeBase selectedNode;
public GameObject targetObject;
private float timeScale = 100f;
private float maxTime = 10.0f;
private float timelineWidth = 100f;
private float timelineHeight = 200f;
private float currentTime = 0f;
private Vector2 scrollPosition;
[MenuItem("Window/Action/ActionTool")]
public static void ShowWindow()
{
GetWindow<ActionToolEditor>("ActionTool");
}
private void AddAnimationNode(string name, float duration)
{
nodes.Add(new AnimationNode() { name = name, startTime = currentTime, duration = duration});
}
private void ShowAddNodeMenu()
{
AddAnimationNode("empty", 0);
}
private void OnGUI()
{
EditorGUILayout.BeginVertical();
targetObject = EditorGUILayout.
ObjectField("Target Object",
targetObject, typeof(GameObject),
true) as GameObject;
timeScale = EditorGUILayout.FloatField("Time Scale", timeScale);
maxTime = EditorGUILayout.FloatField("Max Time", maxTime);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Timeline", EditorStyles.boldLabel);
// 스크롤 뷰 시작
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
Rect timelineRect = GUILayoutUtility.GetRect(timeScale * maxTime, timelineHeight);
DrawTimelineText(timelineRect);
timelineRect.y += 20;
GUI.Box(timelineRect, "");
DrawTimelineGrid(timelineRect);
DrawCurrentTimeline(timelineRect);
DrawNodes();
HandleTimelineInput(timelineRect);
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
Repaint();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Add Animation"))
{
ShowAddNodeMenu();
}
EditorGUILayout.EndHorizontal();
if (selectedNode is AnimationNode animationNode)
{
animationNode.name = EditorGUILayout.TextField("Name", animationNode.name);
animationNode.startTime = EditorGUILayout.FloatField("StartTime", animationNode.startTime);
animationNode.duration = EditorGUILayout.FloatField("Duration", animationNode.duration);
}
}
private void DrawNodes()
{
int space = 10;
foreach (var actionNodeBase in nodes)
{
AnimationNode animNode = actionNodeBase as AnimationNode;
if(animNode != null)
{
float x = (animNode.startTime * timeScale);
if (GUI.Button(new Rect(x, space, animNode.duration * timeScale + 30, 30), animNode.name))
{
selectedNode = animNode;
}
if (currentTime >= animNode.startTime && currentTime <= animNode.startTime + animNode.duration)
{
if (targetObject)
{
if (targetObject.TryGetComponent<Animator>(out var animator))
{
Debug.Log($"{animNode.name} {currentTime - animNode.startTime} {animNode.duration}");
animator.Play(animNode.name
, 0,
(currentTime - animNode.startTime) / animNode.duration);
animator.Update(0.0f);
}
}
}
}
space += 30;
}
}
void DrawTimelineText(Rect timelineRect)
{
float secondWidth = timeScale;
float totalSeconds = maxTime;
for (int i = 0; i < totalSeconds; i++)
{
float x = timelineRect.x + (secondWidth * i);
GUI.Label(
new Rect(x - 15, timelineRect.y, 30, 15),
i.ToString("F1"),
new GUIStyle(EditorStyles.miniLabel) { alignment = TextAnchor.MiddleCenter }
);
}
}
void DrawTimelineGrid(Rect timelineRect)
{
float secondWidth = timeScale;
float totalSeconds = maxTime;
for (int i = 0; i < totalSeconds; i++)
{
float x = timelineRect.x + (secondWidth * i);
Handles.DrawLine(new Vector3(x, timelineRect.y, 0),
new Vector3(x, timelineRect.y + timelineRect.height, 0));
}
}
void DrawCurrentTimeline(Rect timelineRect)
{
float x = timelineRect.x + (currentTime * timeScale);
Handles.color = Color.red;
Handles.DrawLine(new Vector3(x, timelineRect.y), new Vector3(x, timelineRect.y + timelineRect.height));
Handles.color = Color.white;
}
void HandleTimelineInput(Rect timelineRect)
{
Event e = Event.current;
if (timelineRect.Contains(e.mousePosition))
{
if (e.type == EventType.MouseDown || e.type == EventType.MouseDrag && e.button == 0)
{
float clickPosition = e.mousePosition.x - timelineRect.x;
currentTime = Mathf.Clamp(clickPosition / timeScale, 0f, timelineRect.width);
e.Use();
Repaint();
}
}
}
}
결과