몬스터가 플레이어한테로 향하는 로직을 구현하였으니 이제
플레이어 위치로 이동 후 공격을 하는 것과 공격을 했을 시 플레이어 화면에 보여줄 피격 이펙트
그리고 플레이어가 적을 공격하였을 때의 이펙트와 넉백을 구현할것이다
using System;
using UnityEngine;
using UnityEngine.EventSystems;
public class PlayerCombatController : MonoBehaviour
{
[Header("Refs")]
[SerializeField] private Camera cam; // 플레이어가 바라보고 있는 카메라 방향(공격 기준)
[SerializeField] private CharacterStats ownerStats; // 플레이어 자신의 스탯
[Header("Attack")]
[SerializeField] private float attackRange = 3f; // 공격 거리
[SerializeField] private float attackRadius = 0.35f; // SphereCast로 두께가 좀 있는 공격판정 사용
[SerializeField] private float attackCooldown = 0.5f; // 공격 쿨타임
[SerializeField] private float attackDamage = 10f; // 기본 공격 데미지
[Header("Tap Detection")]
[SerializeField] private bool enableTapAttack = true;
[Tooltip("이 시간보다 짧게 눌렀다 떼면 공격으로 인정")]
[SerializeField] private float maxTapTime = 0.22f;
[Tooltip("이 픽셀보다 적게 움직였을 때만 공격으로 인정")]
[SerializeField] private float maxTapMovePixels = 12f;
[Tooltip("UI 위 터치는 공격으로 처리하지 않음")]
[SerializeField] private bool ignoreWhenPointerOverUI = true;
// 화면 이동을 마우스 클릭을 통해 하기 떄문에 오인된 공격 판정을 없에기 위해 사용
// (생각해보니 화면 이동을 클릭이 아닌 그냥 마우스를 따라 가도록 하고, UI 켰을때만 표시)
// 마크처럼 할거면 이렇게 해서는 안됐었음 나중 고칠 예정
[Header("Masks")]
[SerializeField] private LayerMask hitMask = ~0; // 몬스터 찾기
[SerializeField] private LayerMask blockMask = ~0; // 몬스터 앞에 블록이 있는지
// 공격 판정 마스크
[Header("Debug")]
[SerializeField] private bool drawDebugRay = true;
[SerializeField] private bool logDebug = true;
private float lastAttackTime = -999f;
private bool mousePressed; // 클릭
private bool mouseDragged; // 드래그
private bool mouseDownStartedOverUI; // 마우스 입력이 UI위에서 시작하였는지
private float mouseDownTime; // 클릭 시간
private Vector2 mouseDownPosition; // 클릭 위치
private int activeTouchId = -1; // 현재 추적 중인 터치
private bool touchDragged; // 터치 드래그
private bool touchStartedOverUI; // 터치가 ui 위에서 시작 되었는지
private float touchDownTime; // 터치 시작 시간
private Vector2 touchDownPosition; // 터치 시작 위치
// 모바일을 생각하고 만들었더니 pc에서 테스트가 힘들어 pc 테스트 마우스, 모바일 터치 입력
// 두 가지 설정
private void Awake()
{
if (cam == null)
cam = Camera.main;
if (ownerStats == null)
ownerStats = GetComponent<CharacterStats>();
if (ownerStats == null)
ownerStats = GetComponentInParent<CharacterStats>();
if (ownerStats == null)
ownerStats = GetComponentInChildren<CharacterStats>();
}
private void Update()
{
if (!enableTapAttack)
return;
#if UNITY_EDITOR || UNITY_STANDALONE
TickMouseTapAttack();
#else
TickTouchTapAttack();
#endif
// 플랫폼에 따른 입력 처리 방식 나눔
}
private void TickMouseTapAttack()
{
// 마우스 공격
if (Input.GetMouseButtonDown(0))
{
// 마우스 왼쪽 버튼 감지
mousePressed = true;
mouseDragged = false;
mouseDownTime = Time.time;
mouseDownPosition = Input.mousePosition;
mouseDownStartedOverUI = IsPointerOverUIForMouse();
}
if (mousePressed && Input.GetMouseButton(0))
// 마우스를 누르고 있는 동안 실행
{
float moved = Vector2.Distance(mouseDownPosition, Input.mousePosition);
// 처음 누른 위치와 현재 마우스 위치 사이의 거리 계산
if (moved > maxTapMovePixels)
mouseDragged = true;
// 이동량이 기준보다 크면 드래그로 판단
}
if (mousePressed && Input.GetMouseButtonUp(0))
{
// 마우스 왼쪽 버튼에서 놓았을 경우
float heldTime = Time.time - mouseDownTime;
float moved = Vector2.Distance(mouseDownPosition, Input.mousePosition);
// 얼마나 오래 마우스를 눌렀는지 계산
bool isShortTap =
!mouseDragged &&
!mouseDownStartedOverUI &&
heldTime <= maxTapTime &&
moved <= maxTapMovePixels;
// 짧게 터치 한건지 확인
mousePressed = false;
mouseDragged = false;
// 마우스 입력 상태 초기화
if (isShortTap)
Attack();
// 짧은 터치면 공격 함수 호출
}
}
// 모바일용 터치 판정
private void TickTouchTapAttack()
{
if (Input.touchCount <= 0)
return;
for (int i = 0; i < Input.touchCount; i++)
{
Touch touch = Input.GetTouch(i);
if (activeTouchId == -1)
{
if (touch.phase == TouchPhase.Began)
{
activeTouchId = touch.fingerId;
touchDragged = false;
touchDownTime = Time.time;
touchDownPosition = touch.position;
touchStartedOverUI = IsPointerOverUIForTouch(touch.fingerId);
}
continue;
}
if (touch.fingerId != activeTouchId)
continue;
if (touch.phase == TouchPhase.Moved)
{
float moved = Vector2.Distance(touchDownPosition, touch.position);
if (moved > maxTapMovePixels)
touchDragged = true;
}
if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled)
{
float heldTime = Time.time - touchDownTime;
float moved = Vector2.Distance(touchDownPosition, touch.position);
bool isShortTap =
!touchDragged &&
!touchStartedOverUI &&
heldTime <= maxTapTime &&
moved <= maxTapMovePixels &&
touch.phase == TouchPhase.Ended;
activeTouchId = -1;
touchDragged = false;
if (isShortTap)
Attack();
return;
}
}
}
public bool IsAimingAtAttackableTarget()
{
// 공격 가능한지 확인
if (cam == null)
return false;
Ray ray = cam.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f)); // 카메라 중앙 레이 쏘기
Vector3 origin = ray.origin + ray.direction * 0.2f; // 오프셋 0.2
Vector3 dir = ray.direction;
bool foundTarget = TryFindNearestTarget(
origin,
dir,
out CharacterStats targetStats,
out RaycastHit targetHit
// 공격 방향에서 가장 가까운 공격 대상 찾기
);
if (!foundTarget || targetStats == null)
return false;
// 대상을 찾지 못한 경우 공격 불가능
bool blocked = IsBlockedBeforeTarget(origin, dir, targetHit.distance);
// 몬스터 앞에 블록이나 지형이 있는지 검사
return !blocked;
// 막혀 있지 않는 경우에 공격 가능
}
public void Attack()
{
// 공격 처리
if (Time.time < lastAttackTime + attackCooldown)
return;
lastAttackTime = Time.time;
if (cam == null)
{
if (logDebug)
Debug.LogWarning("PlayerCombatController: Camera is null.");
return;
}
Ray ray = cam.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f));
Vector3 origin = ray.origin + ray.direction * 0.2f;
Vector3 dir = ray.direction;
// 화면 중앙 기준으로 공격 레이
if (drawDebugRay)
Debug.DrawRay(origin, dir * attackRange, Color.magenta, 0.5f);
bool foundTarget = TryFindNearestTarget(
origin,
dir,
out CharacterStats targetStats,
out RaycastHit targetHit
);
// 공격 범위 안에서 가장 가까운 공격 대상을 찾기
if (!foundTarget || targetStats == null)
{
if (logDebug)
Debug.Log("Player attack: no target.");
// 대상을 찾지 못한 경우 실패
return;
}
bool blocked = IsBlockedBeforeTarget(origin, dir, targetHit.distance);
// 블록 확인
if (blocked)
{
if (logDebug)
Debug.Log("Player attack: blocked by world before target.");
return;
}
float damage = ownerStats != null ? ownerStats.AttackPower : attackDamage;
// 데미지 계산,
if (damage <= 0f)
damage = attackDamage;
// 공격력이 0 이하면 기본 데미지로 보정, 오류 또는 실수 방지
targetStats.TakeDamage(damage);
// 데미지를 주기
MonsterHitFeedback feedback = targetStats.GetComponent<MonsterHitFeedback>();
// 피격 피드백 컴포넌트 찾기
if (feedback == null)
feedback = targetStats.GetComponentInChildren<MonsterHitFeedback>();
if (feedback == null)
feedback = targetStats.GetComponentInParent<MonsterHitFeedback>();
if (feedback != null)
feedback.PlayHitFeedback(dir);
if (logDebug)
Debug.Log($"Player attacked {targetStats.name} for {damage} damage.");
}
private bool TryFindNearestTarget(
Vector3 origin,
Vector3 dir,
out CharacterStats nearestStats,
out RaycastHit nearestHit)
// 공격 방향에 있는 가장 가까운 공격 가능한 대상 찾기
{
nearestStats = null;
nearestHit = default;
RaycastHit[] hits = Physics.SphereCastAll(
origin,
attackRadius,
dir,
attackRange,
hitMask,
QueryTriggerInteraction.Ignore
);
if (hits == null || hits.Length == 0)
return false;
// 아무것도 맞지 않았다면 실패
Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));
// 감지된 몬스터를 거리순으로 정렬 , 가장 가까운 대상 검사
for (int i = 0; i < hits.Length; i++)
{
Collider col = hits[i].collider;
// 현재 맞은 콜라이더를 가져온다
if (col == null)
continue;
if (IsSelfCollider(col))
continue;
CharacterStats stats = FindStatsFromCollider(col);
if (stats == null)
continue;
if (stats == ownerStats)
continue;
nearestStats = stats;
nearestHit = hits[i];
return true; // 공격 대상 찾음
}
return false; // 공격 대상 없음
}
private bool IsBlockedBeforeTarget(Vector3 origin, Vector3 dir, float targetDistance)
{
// 몬스터 앞에 블록이나 지형이 있는지 검사
RaycastHit[] hits = Physics.RaycastAll(
origin,
dir,
attackRange,
blockMask,
QueryTriggerInteraction.Ignore
);
if (hits == null || hits.Length == 0)
return false;
Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));
// 가까운 순서대로 정렬한다
for (int i = 0; i < hits.Length; i++)
{
Collider col = hits[i].collider;
if (col == null)
continue;
if (IsSelfCollider(col))
continue;
CharacterStats stats = FindStatsFromCollider(col);
if (stats != null && stats != ownerStats)
continue;
if (hits[i].distance < targetDistance - 0.05f)
return true;
}
return false;
}
private CharacterStats FindStatsFromCollider(Collider col)
{
// 캐릭터 스탯 찾기, (몬스터 스탯)
if (col == null)
return null;
CharacterStats stats = col.GetComponent<CharacterStats>();
if (stats == null)
stats = col.GetComponentInParent<CharacterStats>();
if (stats == null)
stats = col.GetComponentInChildren<CharacterStats>();
return stats;
}
private bool IsSelfCollider(Collider col)
{
// 공격 판정에서의 충돌체가 나인지 확인
if (col == null)
return false;
if (ownerStats != null)
{
Transform ownerRoot = ownerStats.transform.root;
if (col.transform.root == ownerRoot)
return true;
}
if (col.transform == transform)
return true;
if (col.transform.IsChildOf(transform))
return true;
return false;
}
private bool IsPointerOverUIForMouse()
{
// 마우스 포인터가 UI 위에 있는지 확인
if (!ignoreWhenPointerOverUI)
return false;
if (EventSystem.current == null)
return false;
return EventSystem.current.IsPointerOverGameObject();
}
private bool IsPointerOverUIForTouch(int fingerId)
{
if (!ignoreWhenPointerOverUI)
return false;
if (EventSystem.current == null)
return false;
return EventSystem.current.IsPointerOverGameObject(fingerId);
}
}
PlayerComboContoller 모바일에서의 근접 공격과 공격 판정 컨트롤러
모바일에서는 화면 터치를 통해 화면을 회전하기 때문에 터치 공격 판정과의 오판별을 없에기 위해 터치 유지 시간과 이동 픽셀 거리를 기준으로 짧게 터치를 한 경우에만 공격 판정으로 읽어낸다
테스트를 하는거는 PC 환경에서 하기 때문에 테스트를 하기에 많이 불편해서 마우스 클릭으로 동일하게 동작시켰고 움직이는거도 키보드 움직임을 따로 INPUT에 추가했다
공격은 화면 중앙을 기준으로 Sphere를 쏘아 일정 반경 내에 들어오는 가장 가까운 적을 찾아 공격하는 방식으로 하였고
공격 성공 시 플레이어의 어택 데미지를 송출해 데미지를 주고 대상이 피격 이벤트가 있다면 연출 하도록 처리 했다
using System.Collections;
using UnityEngine;
public class MonsterHitFeedback : MonoBehaviour
{
[Header("Refs")]
[SerializeField] private Renderer[] renderers; // 색상을 바꿔줄 렌더러(피격 시 빨갛게 하기 위함)
// 배열로 한거는 캐릭터 모델이 보통 몸 머리 이런식으로 분리되어 있으니까
[SerializeField] private CharacterController characterController;
[SerializeField] private MonsterController monsterController;
[Header("Flash")]
[SerializeField] private Color hitColor = new Color(1f, 0.2f, 0.2f, 1f); // 색상 설정
[SerializeField] private float flashDuration = 0.12f; // 색상 변경될 시간
[Header("Knockback")]
[SerializeField] private float knockbackDistance = 0.35f; // 넉백 거리
[SerializeField] private float knockbackDuration = 0.12f; // 넉백 시간
private Coroutine flashCoroutine; // 색상
private Coroutine knockbackCoroutine; // 넉백
// 현재 실행 중인 피격 코루틴 저장, 몬스터가 연속으로 맞았을 경우
private void Awake()
{
// 참조 자동으로 찾기
if (renderers == null || renderers.Length == 0)
renderers = GetComponentsInChildren<Renderer>();
if (characterController == null)
characterController = GetComponent<CharacterController>();
if (monsterController == null)
monsterController = GetComponent<MonsterController>();
}
public void PlayHitFeedback(Vector3 hitDirection)
{
// 외부에서 호출할 피격 이벤트
if (flashCoroutine != null)
StopCoroutine(flashCoroutine); // 이미 빨간색이면 중단
if (knockbackCoroutine != null)
StopCoroutine(knockbackCoroutine); // 이미 넉백 중이면 중단
flashCoroutine = StartCoroutine(FlashRoutine()); // 빨간색 코루틴 실행
Vector3 knockDir = hitDirection; // 공격 방향을 넉백 방향으로
knockDir.y = 0f; // 수평으로 밀리게
if (knockDir.sqrMagnitude < 0.0001f)
knockDir = -transform.forward;
// 넉백 방향이 0이면 기본 방향으로 대체
knockDir.Normalize(); // 넉백 방향 벡터 정규화
if (monsterController != null)
monsterController.LockMovement(knockbackDuration);
// 넉백 시간 동안 이동 잠그기
knockbackCoroutine = StartCoroutine(KnockbackRoutine(knockDir));
// 계산된 넉백 방향으로 코루틴 시작
}
private IEnumerator FlashRoutine()
{
// 피격 시 색상 바꾸는 코루틴
ApplyColor(hitColor); // 빨갛게 바꿔줌
yield return new WaitForSeconds(flashDuration); // 0.12초 기다리기
ClearColor(); // 다시 원래 색상으로
}
private IEnumerator KnockbackRoutine(Vector3 dir)
{
// 넉백 코루틴
float elapsed = 0f; // 넉백이 진행된 시간
float speed = knockbackDuration > 0f ? knockbackDistance / knockbackDuration : 0f;
// 넉백 속도 계산, 거속시
while (elapsed < knockbackDuration)
{
// 넉백 시간 만큼 이동, 여러 프레임에 걸쳐서 부드럽게
float dt = Time.deltaTime;
Vector3 move = dir * speed * dt;
if (characterController != null && characterController.enabled)
characterController.Move(move);
else
transform.position += move;
elapsed += dt;
yield return null;
}
}
private void ApplyColor(Color color)
{
// 몬스터 색상 변경
if (renderers == null)
return;
for (int i = 0; i < renderers.Length; i++)
{
Renderer r = renderers[i];
if (r == null) continue;
MaterialPropertyBlock block = new MaterialPropertyBlock();
r.GetPropertyBlock(block);
// 머테리얼 컬러를 직접 바꾸면 인스턴스가 생성되거나, 공유 머테리얼이 바뀌는 문제가 생길 수 있음
// 그렇기에 머테리얼을 복사하거나 수정하지않고 렌더러에만 색상 값을 덮어 씌움
block.SetColor("_BaseColor", color);
block.SetColor("_Color", color);
r.SetPropertyBlock(block);
}
}
private void ClearColor()
{
// 원래 색상으로 돌리기
if (renderers == null)
return;
for (int i = 0; i < renderers.Length; i++)
{
Renderer r = renderers[i];
if (r == null) continue;
// 덮어 씌운거 제거
r.SetPropertyBlock(null);
}
}
}
MonsterHitFeedback 몬스터가 플레이어 공격에 맞았을 경우 피격 이벤트 몬스터 색상 변경과 공격 방향으로의 넉백
색상의 변경은 머테리얼은 직접 수정하지 않고 MaterialPropertyBlock을 사용해 Renderer별로 임시 색상 값을 적용
넉백 중에는 움직이는 것을 막아 충돌을 피해줌
using UnityEngine;
using UnityEngine.UI;
public class PlayerHealthUI : MonoBehaviour
{
[Header("Refs")]
[SerializeField] private CharacterStats playerStats;
[Header("UI")]
[SerializeField] private Image hpFillImage; // Fill Bar Image
[SerializeField] private Text hpText; // hp 숫자 표시
private void Awake()
{
if (playerStats == null)
{
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
if (playerObj != null)
playerStats = playerObj.GetComponent<CharacterStats>();
}
}
private void OnEnable()
{
// 이벤트 구독 활성화
if (playerStats != null)
{
playerStats.OnHealthChanged += HandleHealthChanged;
playerStats.OnDied += HandleDied;
}
Refresh();
}
private void OnDisable()
{
// 이벤트 구독 해제
if (playerStats != null)
{
playerStats.OnHealthChanged -= HandleHealthChanged;
playerStats.OnDied -= HandleDied;
}
}
private void HandleHealthChanged(CharacterStats stats)
{
// 이벤트 발생시 호출
Refresh();
}
private void HandleDied(CharacterStats stats)
{
// 플레이어 사망 시 호출
Refresh();
Debug.Log("Player died.");
}
private void Refresh()
{
if (playerStats == null)
return;
float max = Mathf.Max(1f, playerStats.MaxHealth); // 최대 체력
float current = Mathf.Clamp(playerStats.CurrentHealth, 0f, max); // 현재 체력
float ratio = current / max; // 현재 체력을 기준 비율, 필바 채우기 위함
if (hpFillImage != null)
hpFillImage.fillAmount = ratio;
if (hpText != null)
hpText.text = $"{Mathf.CeilToInt(current)} / {Mathf.CeilToInt(max)}";
// Fill Bar 이미지 갱신
}
}
PlayerHealthUI 플레이어의 스탯을 통해 플레이어 UI에 표시해주는 HUD 컴포넌트
현재 체력과 최대 체력의 비율을 계산하여 Fill Bar에 송출하여 표시해줌, 0이하인 경우의 오류를 막기 위해 최소 최대 체력을 1로 보정 해줌 비정상적인 범위를 벗어나지 못하도록
sing System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class PlayerDamageFeedback : MonoBehaviour
{
[Header("Refs")]
[SerializeField] private CharacterStats playerStats;
[SerializeField] private Image damageFlashImage; // 피격 시 표시될 UI 이미지
[Header("Flash")]
[SerializeField] private float flashAlpha = 0.35f; // 알파값 투명도
[SerializeField] private float flashDuration = 0.15f; // 지속 시간
// 피격 시 보여줄 이펙트의 강도와 지속 시간, 빨간색으로 화면에 띄워줌
private Coroutine flashCoroutine; // 실행 중인 코루틴
private float lastHealth; // 이전 체력 저장
private void Awake()
{
if (playerStats == null)
{
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
if (playerObj != null)
playerStats = playerObj.GetComponent<CharacterStats>();
}
if (playerStats != null)
lastHealth = playerStats.CurrentHealth;
ClearFlash();
}
private void OnEnable()
{
// 체력 변경 이벤트 구독
if (playerStats != null)
playerStats.OnHealthChanged += HandleHealthChanged;
}
private void OnDisable()
{
// 구독 해제
if (playerStats != null)
playerStats.OnHealthChanged -= HandleHealthChanged;
}
private void HandleHealthChanged(CharacterStats stats)
{
// 체력이 줄어든 경우 피격 이벤트 호출
if (stats.CurrentHealth < lastHealth)
{
PlayFlash();
}
lastHealth = stats.CurrentHealth;
}
private void PlayFlash()
{
// 이벤트 코루틴 시작 함수
if (damageFlashImage == null)
return;
if (flashCoroutine != null)
StopCoroutine(flashCoroutine);
flashCoroutine = StartCoroutine(FlashRoutine());
}
private IEnumerator FlashRoutine()
{
// 화면을 빨갛게 만들어줌
Color color = damageFlashImage.color;
color.a = flashAlpha;
damageFlashImage.color = color;
float elapsed = 0f;
while (elapsed < flashDuration)
{
// 시간이 될때까지 재생
elapsed += Time.deltaTime;
float t = elapsed / flashDuration;
color.a = Mathf.Lerp(flashAlpha, 0f, t);
damageFlashImage.color = color;
// 시간에 따라 알파값을 서서히 줄여줌
yield return null;
}
ClearFlash();
}
private void ClearFlash()
{
// 다시 완전히 투명하게 만들어줌
if (damageFlashImage == null)
return;
Color color = damageFlashImage.color;
color.a = 0f;
damageFlashImage.color = color;
}
}
PlayerDamageFeedback 플레이어가 데미지를 받았을 경우 화면 전체에 빨간색 플레시를 표출하는 피격 피드백 UI 컴포넌트
체력 데이터를 가져와 현재 체력이 이전 체력보다 감소했을 경우 피격 효과를 발생 시킴
빨간색으로 된 이미지를 화면전체에 크게 넣어주고 알파값을 0으로해 평소 투명한 상태로 두었다가 피격 시 알파값을 올려주고
반복문을 통해 서서히 낮추는 식으로 동작 해줌
using UnityEngine;
using UnityEngine.EventSystems;
public class LookAreaTapAttack : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler
// UI 인터페이스 순서대로, 터치/마우스를 누른 순간, 뗀 순간, 드래그 중
{
[Header("Refs")]
[SerializeField] private PlayerCombatController combatController; // 공격 참조
[Header("Tap Detection")]
[SerializeField] private float maxTapTime = 0.22f;
[SerializeField] private float maxTapMovePixels = 12f;
// 가벼운 터치 인지 판단 기준값
private float downTime;
private Vector2 downPosition;
private bool dragged;
// 사용자가 누른 순간부터 뗄 때까지의 상태 저장
private void Awake()
{
if (combatController == null)
combatController = FindObjectOfType<PlayerCombatController>();
}
public void OnPointerDown(PointerEventData eventData)
{
// 눌렀을 경우 호출 저장
downTime = Time.time;
downPosition = eventData.position;
dragged = false;
}
public void OnDrag(PointerEventData eventData)
{
// 누른 상태로 움직인 경우 호출 (드래그 상태)
float moved = Vector2.Distance(downPosition, eventData.position);
if (moved > maxTapMovePixels)
dragged = true;
}
public void OnPointerUp(PointerEventData eventData)
{
// 뗀 순간 호출하여 공격 판단
float heldTime = Time.time - downTime;
float moved = Vector2.Distance(downPosition, eventData.position);
bool isShortTap =
!dragged &&
heldTime <= maxTapTime &&
moved <= maxTapMovePixels;
if (isShortTap && combatController != null)
{
combatController.Attack();
}
}
}
LookAreaTapAttack 시점 이동에서 공격에 대하 여부 판단
누른 시간 드래그 뗀 시간을 비교하여 가볍게 누른 것인지를 판단해 공격인지 시점 회전인지를 판단한다.
플레이어가 공격을 했을 때의 몬스터에게서 나올 이벤트와 몬스터가 플레이어를 공격했을 때의 이벤트 데미지 UI를 하였다
이렇게 게임 내의 간단한 기본적인 구현은 하였고 세부 구현은
게임 로비를 만들고 로비에서 게임 시작 스테이지 설정 이런식으로 게임의 기능적인 부분을 하고 나서 구현하겠다.
아래는 오늘 한거 까지의 플레이 영상이다 저번과 마찬가지로 파일로 올렸다
'Unity' 카테고리의 다른 글
| Unity(11) - Stage Select & Lobby (0) | 2026.05.31 |
|---|---|
| Unity(9) - Random Spawn Monster & A* (0) | 2026.04.27 |
| Unity(8) - Map Error Resolution (0) | 2026.04.13 |
| Unity(7) - 3x3 Crafting Table (0) | 2026.03.13 |
| Unity(6) - Atlas Block Map & Crafting Table (2) (0) | 2026.03.10 |