안녕하세요.😊
이번 작업은 Enumy를 공격해서 Enumy의 Hp Bar가 줄어드는 작업을 진행했습니다.
가장 먼저 이전에 작업했던 Skill에 애니메이션 전이 조건을 실행되게 작업하려던 도중
스킬을 발사할 때 원하던 방향으로 발사체가 날아가지만,
원하던 위치에 생기지 않는 문제를 발견했습니다.
수정 전 코드입니다.
skill.skill1 Object.transform.position = playerTransform.position + new Vector3(0f, 1f, 0.4f);
스킬 오브젝트의 포지션이 생기는 위치를
플레이어의 기준 + 살짝 위 방향에서 z 축 0.4f의 위치에 생성되도록 했었는데요
바로 원인을 살펴보자면
단순히 봤을 땐 이상이 없어 보이지만!
새로 만든 new Vector3()가 캐릭터의 전방 기준이 아닌
월드 전방 기준으로 적용되던 문제였습니다.
수정 후 코드
skill.skill1 Object.transform.position = playerTransform.position + playerTransform.TransformDirection(new Vector3(0f, 0.5f, 0.4f));
즉 플레이어가 어느 위치에 있던 플레이어의 월드 축 (z = 0.4f)가 되던 문제로
방향벡터를 오브젝트의 현재 회전에 맞춰서 월드 좌표계 방향으로 변환해 주는 함수
playerTransform.TransformDirection()를 사용하여 수정해 주었습니다.
가장 먼저 PlayerSkillController 스크립트의 수정 및 추가된 부분부터 살펴보겠습니다.
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections.Generic;
using UnityEngine.AI;
using System.Collections;
public class PlayerSkillController : MonoBehaviour
{
[Serializable]
public class Skill
{
public Image skillImage;
public TextMeshProUGUI skillText;
public KeyCode skillKeyCode;
public float cooldown;
[HideInInspector] public bool isCoolingDown = false;
[HideInInspector] public float currentCooldown = 0f;
public GameObject skillRangeQuadParent;
public GameObject skillObject;
[HideInInspector] public Vector3 skillStartPos;
[HideInInspector] public bool skillIsMoving = false;
public float skillMoveDistance = 5f;
public float skillMoveSpeed = 2f;
[HideInInspector] public Vector3 skillMoveDirection = Vector3.zero;
}
[SerializeField] private Transform playerTransform;
[SerializeField] private Animator playerAnim;
[SerializeField] private NavMeshAgent playerNav;
[SerializeField] private List<Skill> skills;
private Vector3? skillETargetPos = null;
[SerializeField] private Skill fireObjectInpomation;
public bool isSkill01 = false;
public bool isSkill02 = false;
public bool isSkill03 = false;
public bool isSkill04 = false;
void Start()
{
foreach (var skill in skills)
{
if (skill.skillImage != null)
skill.skillImage.fillAmount = 0f;
if (skill.skillText != null)
skill.skillText.text = "";
if (skill.skillRangeQuadParent != null)
skill.skillRangeQuadParent.gameObject.SetActive(false);
if (skill.skillObject != null)
{
// skill.skillStartPos = skill.skillObject.transform.position;
skill.skillObject.SetActive(false);
}
}
}
void Update()
{
foreach (var skill in skills)
{
HandleSkillInput(skill);
HandleSkillCooldown(skill);
HandleSkillMovement(skill);
}
}
private void HandleSkillInput(Skill skill)
{
if (Input.GetKey(skill.skillKeyCode) && !skill.isCoolingDown)
{
if (!skill.skillRangeQuadParent.activeSelf)
skill.skillRangeQuadParent.gameObject.SetActive(true);
if (skill == skills[2])
{
Plane groundPlane = new Plane(Vector3.up, playerTransform.position);
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
float enter;
if (groundPlane.Raycast(ray, out enter))
{
Vector3 hitPoint = ray.GetPoint(enter);
Vector3 playerPos = playerTransform.position;
playerPos.y = 0;
Vector3 targetPos = hitPoint;
targetPos.y = 0;
Vector3 dir = targetPos - playerPos;
// 최대 거리 제한
if (dir.magnitude > skill.skillMoveDistance)
{
dir = dir.normalized * skill.skillMoveDistance;
targetPos = playerPos + dir;
}
if (!skill.skillRangeQuadParent.activeSelf)
skill.skillRangeQuadParent.SetActive(true);
if (skill.skillObject != null)
{
skill.skillObject.SetActive(true);
skill.skillObject.transform.position = new Vector3(targetPos.x, playerTransform.position.y + 0.01f, targetPos.z);
skillETargetPos = skill.skillObject.transform.position;
}
}
}
else if (skill == skills[0] || skill == skills[1] || skill == skills[3])
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, Mathf.Infinity))
{
Vector3 dir = hit.point - skill.skillRangeQuadParent.transform.position;
dir.y = 0;
skill.skillRangeQuadParent.transform.rotation = Quaternion.LookRotation(dir);
}
}
}
if (Input.GetKeyUp(skill.skillKeyCode) && !skill.isCoolingDown)
{
skill.isCoolingDown = true;
skill.currentCooldown = skill.cooldown;
skill.skillRangeQuadParent.gameObject.SetActive(false);
if (skill == skills[2])
{
if (skillETargetPos.HasValue)
{
Vector3 teleportPos = skillETargetPos.Value;
teleportPos.y = playerTransform.position.y;
Vector3 lookDir = (teleportPos - playerTransform.position);
lookDir.y = 0;
if (lookDir.sqrMagnitude > 0.01f)
{
playerTransform.rotation = Quaternion.LookRotation(lookDir.normalized);
}
if (playerNav != null)
{
StartCoroutine(SkillEDelay(teleportPos));
}
else
{
playerTransform.position = teleportPos;
}
skillETargetPos = null;
}
if (skill.skillObject != null)
skill.skillObject.SetActive(false);
}
else if (skill == skills[0] || skill == skills[1] || skill == skills[3])
{
if (playerTransform != null)
{
Vector3 lookDir = skill.skillRangeQuadParent.transform.forward;
lookDir.y = 0;
if (lookDir.sqrMagnitude > 0.01f)
{
playerTransform.rotation = Quaternion.LookRotation(lookDir);
skill.skillMoveDirection = lookDir.normalized;
}
}
if (skill.skillObject != null)
{
}
if (skill != null)
{
if (skill == skills[0] || skill == skills[1])
{
if (skill == skills[0])
{
playerAnim.SetBool("IsSkillQ", true);
}
else if (skill == skills[1])
{
playerAnim.SetBool("IsSkillW", true);
}
ObjectGen(skill);
}
else if (skill == skills[3])
{
if (skill == skills[3])
{
StartCoroutine(SkillRDelay(skill));
}
}
}
}
}
}
private IEnumerator SkillEDelay(Vector3 pos)
{
isSkill03 = true;
playerAnim.SetTrigger("IsSkillE");
yield return new WaitForSeconds(0.5f);
playerNav.Warp(pos);
yield return new WaitForSeconds(0.5f);
isSkill03 = false;
}
private IEnumerator SkillRDelay(Skill skill)
{
isSkill04 = true;
playerAnim.SetTrigger("IsSkillR");
yield return new WaitForSeconds(1f);
isSkill04 = false;
ObjectGen(skill);
}
private void ObjectGen(Skill skill)
{
print("33");
skill.skillObject.SetActive(true);
skill.skillIsMoving = true;
skill.skillObject.transform.position = playerTransform.position + playerTransform.TransformDirection(new Vector3(0f, 0.5f, 0.4f));
skill.skillMoveDirection = playerTransform.forward.normalized;
skill.skillStartPos = skill.skillObject.transform.position;
skill.skillObject.transform.rotation = Quaternion.LookRotation(skill.skillMoveDirection);
}
private void HandleSkillCooldown(Skill skill)
{
if (skill.isCoolingDown)
{
skill.currentCooldown -= Time.deltaTime;
if (skill.currentCooldown <= 0f)
{
skill.isCoolingDown = false;
skill.currentCooldown = 0f;
if (skill.skillImage != null)
skill.skillImage.fillAmount = 0f;
if (skill.skillText != null)
skill.skillText.text = "";
}
else
{
if (skill.skillImage != null)
skill.skillImage.fillAmount = skill.currentCooldown / skill.cooldown;
if (skill.skillText != null)
skill.skillText.text = Mathf.Ceil(skill.currentCooldown).ToString();
}
}
}
private void HandleSkillMovement(Skill skill)
{
if (!skill.skillIsMoving || skill.skillObject == null)
return;
fireObjectInpomation = skill;
skill.skillObject.transform.position += skill.skillMoveDirection * skill.skillMoveSpeed * Time.deltaTime;
float traveledDist = Vector3.Distance(skill.skillObject.transform.position, skill.skillStartPos);
if (traveledDist >= skill.skillMoveDistance)
{
ResetSkillObject();
}
}
public void ResetSkillObject()
{
if (fireObjectInpomation.skillObject == null)
return;
// skill.skillObject.transform.position = skill.skillStartPos;
if (fireObjectInpomation.skillObject != null)
{
fireObjectInpomation.skillObject.SetActive(false);
}
fireObjectInpomation.skillIsMoving = false;
playerAnim.SetBool("IsSkillQ", false);
playerAnim.SetBool("IsSkillW", false);
fireObjectInpomation = null;
}
}
1. 먼저 스킬에 각 애니메이션을 적용시켜 주기 위해 스킬 키를 떼었을 때 작업을 해주었습니다.
if (Input.GetKeyUp(skill.skillKeyCode) && !skill.isCoolingDown)
{
skill.isCoolingDown = true;
skill.currentCooldown = skill.cooldown;
skill.skillRangeQuadParent.gameObject.SetActive(false);
if (skill == skills[2])
{
if (skillETargetPos.HasValue)
{
Vector3 teleportPos = skillETargetPos.Value;
teleportPos.y = playerTransform.position.y;
Vector3 lookDir = (teleportPos - playerTransform.position);
lookDir.y = 0;
if (lookDir.sqrMagnitude > 0.01f)
{
playerTransform.rotation = Quaternion.LookRotation(lookDir.normalized);
}
if (playerNav != null)
{
StartCoroutine(SkillEDelay(teleportPos));
}
else
{
playerTransform.position = teleportPos;
}
skillETargetPos = null;
}
if (skill.skillObject != null)
skill.skillObject.SetActive(false);
}
else if (skill == skills[0] || skill == skills[1] || skill == skills[3])
{
if (playerTransform != null)
{
Vector3 lookDir = skill.skillRangeQuadParent.transform.forward;
lookDir.y = 0;
if (lookDir.sqrMagnitude > 0.01f)
{
playerTransform.rotation = Quaternion.LookRotation(lookDir);
skill.skillMoveDirection = lookDir.normalized;
}
}
if (skill.skillObject != null)
{
}
if (skill != null)
{
if (skill == skills[0] || skill == skills[1])
{
if (skill == skills[0])
{
playerAnim.SetBool("IsSkillQ", true);
}
else if (skill == skills[1])
{
playerAnim.SetBool("IsSkillW", true);
}
ObjectGen(skill);
}
else if (skill == skills[3])
{
if (skill == skills[3])
{
StartCoroutine(SkillRDelay(skill));
}
}
}
}
}
}
1.1 스킬 "E" 처리 부분입니다.
스킬이 즉시 이동되는 것이 원하던 연출이 아니었기에 코루틴으로 시간을 지연시켜 주게 작업을 해주었습니다.
if (skill == skills[2])
{
if (skillETargetPos.HasValue)
{
Vector3 teleportPos = skillETargetPos.Value;
teleportPos.y = playerTransform.position.y;
Vector3 lookDir = (teleportPos - playerTransform.position);
lookDir.y = 0;
if (lookDir.sqrMagnitude > 0.01f)
{
playerTransform.rotation = Quaternion.LookRotation(lookDir.normalized);
}
if (playerNav != null)
{
StartCoroutine(SkillEDelay(teleportPos));
}
else
{
playerTransform.position = teleportPos;
}
skillETargetPos = null;
}
if (skill.skillObject != null)
skill.skillObject.SetActive(false);
}
1.2 스킬 "Q, W, R" 처리 부분입니다.
기본 로직에서 크게 달라진 건 없지만 특이사항으로
스킬 Q와 W는 딜레이를 주지 않고
스킬 R 같은 경우는 딜레이를 주었습니다.
else if (skill == skills[0] || skill == skills[1] || skill == skills[3])
{
if (playerTransform != null)
{
Vector3 lookDir = skill.skillRangeQuadParent.transform.forward;
lookDir.y = 0;
if (lookDir.sqrMagnitude > 0.01f)
{
playerTransform.rotation = Quaternion.LookRotation(lookDir);
skill.skillMoveDirection = lookDir.normalized;
}
}
if (skill.skillObject != null)
{
}
if (skill != null)
{
if (skill == skills[0] || skill == skills[1])
{
if (skill == skills[0])
{
playerAnim.SetBool("IsSkillQ", true);
}
else if (skill == skills[1])
{
playerAnim.SetBool("IsSkillW", true);
}
ObjectGen(skill);
}
else if (skill == skills[3])
{
if (skill == skills[3])
{
StartCoroutine(SkillRDelay(skill));
}
}
}
}
2. 스킬 "E" 일 때 사용되는 코루틴입니다.
애니메이션의 전이 조건을 즉시 실행시켜 주었으며
0.5초 후 대기시간을 준 뒤 NavMeshAgent의 Warp를 활용해 플레이어를 텔레포트시켜 주었습니다.
다음 0.5초의 딜레이를 주는 이유는 단순히 연출 효과인데
플레이어가 스킬 E를 쓰고 자연스럽게 애니메이션이 "IdelToRun"으로 돌아오게 한 뒤 이동이 되게 하기 위함입니다.
private IEnumerator SkillEDelay(Vector3 pos)
{
isSkill03 = true;
playerAnim.SetTrigger("IsSkillE");
yield return new WaitForSeconds(0.5f);
playerNav.Warp(pos);
yield return new WaitForSeconds(0.5f);
isSkill03 = false;
}
3. 스킬 "R" 일 때 사용되는 코루틴입니다.
일명 롤에서 "궁극기"라고도 불리는 스킬 "R"은 그에 걸맞게(?) 딜레이를 좀 더 주었으며 (1f)
ObjectGen() 함수를 실행시켜 주었습니다.
private IEnumerator SkillRDelay(Skill skill)
{
isSkill04 = true;
playerAnim.SetTrigger("IsSkillR");
yield return new WaitForSeconds(1f);
isSkill04 = false;
ObjectGen(skill);
}
4. 다음으로 스킬 "Q, W, R"이 실행되면 호출될 ObjectGen() 함수입니다.
기존에 Input.GetKeyUp()이 실행될 때 작성되어 있던 코드를
함수로 나누어 재사용을 해주었습니다.
private void ObjectGen(Skill skill)
{
skill.skillObject.SetActive(true);
skill.skillIsMoving = true;
skill.skillObject.transform.position = playerTransform.position + playerTransform.TransformDirection(new Vector3(0f, 0.5f, 0.4f));
skill.skillMoveDirection = playerTransform.forward.normalized;
skill.skillStartPos = skill.skillObject.transform.position;
skill.skillObject.transform.rotation = Quaternion.LookRotation(skill.skillMoveDirection);
}
5. 다음은 스킬 오브젝트의 리셋 함수입니다.
이 리셋 함수는 오브젝트가 일정 거리 이상 이동했을 때와, Enumy에게 맞았을 때 호출 됩니다.
특별히 추가가 된 곳은 애니메이션의 전이 조건을 False로 해주어
반복 실행이 안되게 해 주었습니다.
public void ResetSkillObject()
{
if (fireObjectInpomation.skillObject == null)
return;
// skill.skillObject.transform.position = skill.skillStartPos;
if (fireObjectInpomation.skillObject != null)
{
fireObjectInpomation.skillObject.SetActive(false);
}
fireObjectInpomation.skillIsMoving = false;
playerAnim.SetBool("IsSkillQ", false);
playerAnim.SetBool("IsSkillW", false);
fireObjectInpomation = null;
}
6. 다음은 스킬 Q에 부착될 스크립트 "PlayerQ"를 만들어 주었습니다.
OnTriggerEnter()로 콜라이더가 충돌될 때 Enumy의 기존에 만들어둔 "EnumyStats" 스크립트에 접근하여
TakeDamage() 함수를 실행시켜 주었습니다.
(Q와 동일하게 W, R 스크립트도 같습니다 (E는 공격 스킬이 아니기에 애니메이션만 실행되게 해 주었습니다.))
using UnityEngine;
public class PlayerSkillQ : MonoBehaviour
{
[SerializeField] private PlayerSkillController playerSkillController;
[SerializeField] private PlayerStats playerStats;
void OnTriggerEnter(Collider other)
{
if (other.tag == "Enumy")
{
EnumyStats enumyStats = other.gameObject.GetComponent<EnumyStats>();
enumyStats?.TakeDamage(playerStats.playerDamage * playerStats.playerSkillQDamage);
playerSkillController.ResetSkillObject();
}
}
}
7. 플레이어의 기본 스텟입니다.
임시로 만들어둔 Hp, Damage, Attack Speed에 더해
Q, W, R의 대미지를 선언해 주었습니다.
(스킬 Q 대미지 = PlayerDamage * PlayerSkillQDamage)
다음 TakeDamage() 함수는 아직 미구현입니다.
using UnityEngine;
public class PlayerStats : MonoBehaviour
{
private float playerHp = 100f;
public float playerDamage = 10f;
public float playerSkillQDamage = 2f;
public float playerSkillWDamage = 0f;
public float playerSkillRDamage = 4f;
public float playerAttackSpeed = 3f;
public void TakeDamage(GameObject player, float damage)
{
playerHp -= damage;
if (playerHp <= 0)
{
Destroy(this.gameObject);
}
}
}
8. 다음은 "EnumyStats" 스크립트입니다.
선언된 변수는 다음과 같습니다.
최대 체력 / 현재 체력 / 공격력 / 공격 스피드
EnumyStats의 TakeDamage 함수는 인자로 damage를 받아서
(현재 체력 = 현재 체력 - 대미지)를 적용시켜 주었습니다.
또한 EnumyHpBar의 스크립트를 새로 생성하여 UI - Hp Bar(Slider)를 따로 관리해 주었습니다.
using UnityEngine;
public class EnumyStats : MonoBehaviour
{
[SerializeField] private EnumyHpBar enumyHpBar;
public float enumyMaxHp = 100f;
public float enumyHp = 100f;
public float enumyDamage = 10f;
public float enumyAttackSpeed = 3f;
public void TakeDamage(float damage)
{
print("11");
enumyHp -= damage;
enumyHpBar.HpVarUpdate(enumyHp, enumyMaxHp);
if (enumyHp <= 0)
{
Destroy(this.gameObject);
}
}
}
9. EnumyHpBar 스크립트입니다.
Enumy에 부착되어 있는 Slider를 가져와
Slider.value를 제어해 주었으며
Slider.value의 최솟값: 0 / 최댓값:1입니다.
using UnityEngine;
using UnityEngine.UI;
public class EnumyHpBar : MonoBehaviour
{
[SerializeField] private Slider sliderHpBar;
public void HpVarUpdate(float hp, float maxHp)
{
// print("22");
sliderHpBar.value = hp / maxHp;
}
}
10. 마지막으로 플레이어의 이동을 제어하는 스크립트인 "PlayerMovement"에서 다음과 같이 조건문을 걸어주었습니다.
특이사항으로, 내비게이션의 목적지 초기화 및 속도 제어와 멈춤을 해준 이유는
플레이어가 이동 중 일 때 스킬을 사용할 수 있기 때문입니다.
이러한 조건문이 없는 상태로 이동도중 스킬을 발사하게 되면
스킬 애니메이션이 진행되면서 플레이어가 이동을 하는 상황이 나오게 됩니다.
private void MoveController()
{
if (playerSkillController.isSkill03 || playerSkillController.isSkill04)
{
playerNav.isStopped = true;
playerNav.ResetPath();
playerNav.velocity = Vector3.zero;
return;
}
if (Input.GetMouseButtonDown(1))
{
RaycastHit hit;
if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out hit, Mathf.Infinity))
{
if (hit.collider.CompareTag("Ground"))
{
PlayerNavToMove(hit.point);
PlayerRotation(hit.point);
}
else if (hit.collider.CompareTag("Enumy"))
{
if (outline != null)
outline.enabled = false;
outline = hit.transform.GetComponent<Outline>();
PlayerNavToEnumy(hit.collider.transform);
}
}
}
if (enumyGameobject != null)
{
if (Vector3.Distance(transform.position, enumyGameobject.position) > stopDistance)
{
playerNav.SetDestination(enumyGameobject.position);
}
}
}
Player Animator
-참고-
작업된 내용을 영상으로 살펴보겠습니다.
아직 전체적으로 뭔가 자연스럽지 않으면서.. 어색한 부분들이 많은 것 같습니다.
그래도 "첫 술에 배부르랴"라는 속담이 있지 않습니까? 하하
Rome wasn’t built in a day
앞으로도 프로젝트의 모든 과정은 최대한 기록을 할 예정이며
과정과 결과로 보여드리겠습니다.
감사합니다.😌