using System.Collections;
using UnityEngine;
using UnityEngine.AI;
public class Actor : MonoBehaviour
{
[HideInInspector] public ActorVisualHandler visualHandler;
private NavMeshAgent agent;
private Animator animator;
//角色需要一个当前的攻击目标,可受伤的攻击目标Damageable
private Damageable damageableTarget;
private AnimationEventListener animationEvent;
public int attack = 10; //角色攻击力 = 10,如果采集物的生命值=100,就要敲击10次
private Coroutine currentTask;
public GameObject hitEffect;
private void Start()
{
visualHandler = GetComponent<ActorVisualHandler>();
agent = GetComponent<NavMeshAgent>();
animator = GetComponentInChildren<Animator>();
animationEvent = GetComponentInChildren<AnimationEventListener>();
animationEvent.attackEvent.AddListener(Attack); //将Attack作为事件,在动画帧上执行
animationEvent.attackEvent.AddListener(AttackEffect);
}
private void Update()
{
//Animator是通过人物的速度Speed来切换不同的动画状态片段的
animator.SetFloat("Speed", Mathf.Clamp(agent.velocity.magnitude, 0, 1));
}
//人物的移动, _target表示人物应该移动到的鼠标位置,由于是未知的,所以设置为参数
public void SetDestination(Vector3 _target)
{
agent.SetDestination(_target);
}
public void Attack()
{
if (damageableTarget)
damageableTarget.Hit(attack);
}
//方法:采集/攻击,这个方法将会在ActorManager脚本中的另一种情况下调用
public void AttackTarget(Damageable _target)
{
StopTask(); //这里你要想到一个问题就是,每次采集的都是不同的,所以要忘记上次的任务
damageableTarget = _target;
//先走到采集物/敌人的附近,再开始采集/攻击
currentTask = StartCoroutine(StartAttack());
}
IEnumerator StartAttack()
{
//情况1: 有点击到敌人,敌人还活着,能采集,继续采集
while (damageableTarget)
{
//首先,我们要走到这个地方啊
SetDestination(damageableTarget.transform.position);
//那走到啥位置停下来呢?
yield return new WaitUntil(() => agent.remainingDistance <= agent.stoppingDistance && !agent.pathPending);
while (damageableTarget && Vector3.Distance(damageableTarget.transform.position, transform.position) < 4f)
{
yield return new WaitForSeconds(1); //每间隔1秒攻击一次,如果太短的话可能会在采集物消失后,多一次攻击动画
if (damageableTarget)
{
animator.SetTrigger("Attack"); //调用Attack动画,采集物消失后,由于角色速度小于等于0.01,就回到Idle动画片段
//Instantiate(hitEffect, damageableTarget.transform.position, Quaternion.identity);//别写在这个地方
}
}
}
//情况2: 采集完了,没东西了
currentTask = null;
}
public void StopTask()
{
damageableTarget = null; //首先,让采集目标为空
if (currentTask != null) //停止正在手头的这份工作,即:立即停止协程的执行
StopCoroutine(currentTask);
}
public void AttackEffect()
{
if (damageableTarget)
Instantiate(hitEffect, damageableTarget.transform.position, Quaternion.identity);
}
}
ActorManager
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ActorManager : MonoBehaviour
{
public static ActorManager instance;//单例模式
[SerializeField] private Transform selectedArea;//框选的范围,实际是一个立方体,一会显示框选范围
public List<Actor> allActors = new List<Actor>();//场景中所有己方角色
[SerializeField] private List<Actor> selectedActors = new List<Actor>();//当前选中的角色(们)
//鼠标拖拽的起始点,终点,和计算显示的框选Cube的中心位置和实际大小,用于localScale的实现
private Vector3 dragStartPos, dragEndPos, dragCenter, dragSize;
public LayerMask mouseDragLayerMask;
private bool isDragging;
public LayerMask dragSelectedLayerMask;//只框选角色,即BoxCastAll方法中的指定层
private void Awake()
{
if(instance == null)
{
instance = this;
}
else
{
if (instance != this)
Destroy(gameObject);
}
DontDestroyOnLoad(gameObject);//防止场景转换时,保持唯一性
}
private void Start()
{
selectedArea.gameObject.SetActive(false);//一开始框选是不可见的
//所有在场景中的角色,都应该添加到allActors这个List中
foreach(Actor actor in GetComponentsInChildren<Actor>())
{
allActors.Add(actor);//相当于游戏开始后完成了“注册”的工作
}
}
private void Update()
{
MouseInput();
}
private void MouseInput()
{
if(Input.GetMouseButtonDown(0))//按下鼠标「开始拖拽」时,需要存储这个点在【世界坐标系】下的位置信息
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
//if(Physics.Raycast(ray, out RaycastHit raycastHit, 100, LayerMask.GetMask("Level")))
if (Physics.Raycast(ray, out RaycastHit raycastHit, 100, mouseDragLayerMask))
{
dragStartPos = raycastHit.point;//raycastHit结构体,out是输出参数,作为方法的第二个输出使用
}
}
else if (Input.GetMouseButton(0))//「按住」鼠标左键的时候
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit raycastHit, 100, mouseDragLayerMask))
{
dragEndPos = raycastHit.point;//raycastHit结构体,out是输出参数,作为方法的第二个输出使用
}
//这里我们希望的是只有拖动了一段距离以后,才会出现框选的范围,而不是细微的变化就会出现框选
if(Vector3.Distance(dragStartPos, dragEndPos) > 1)
{
isDragging = true;//正在拖动
selectedArea.gameObject.SetActive(true);//框选范围可见
//生成一个范围大小和坐标了
dragCenter = (dragStartPos + dragEndPos) / 2;//0, 20 -> 10 | -40, 90 -> 25
dragSize = dragEndPos - dragStartPos;
selectedArea.transform.position = dragCenter;
selectedArea.transform.localScale = dragSize + Vector3.up;//提高一点框选可见范围的空中高度
}
}
else if(Input.GetMouseButtonUp(0))
{
//情况1: 我们之前还在拖动,范围内全选的工作
if(isDragging == true)
{
//松开dragSelectedLayerMask
SelectActors();
isDragging = false;
selectedArea.gameObject.SetActive(false);
}
else//情况2: 我们之前其实没有在拖动,这时候鼠标松开其实纯碎只是鼠标左键的点击,可能是角色的移动、攻击/采集
{
SetTask();
}
}
//if(UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject())
//{
// isDragging = false;
// selectedArea.gameObject.SetActive(false);
// return;
//}
}
private void SetTask()
{
//Debug.Log("Set Task!");
//首先要注意一点,如果我们框选空气的话,或者随便点击左键,触发这个方法都不该有什么逻辑执行
if (selectedActors.Count == 0)
return;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
Collider collider;
if(Physics.Raycast(ray, out RaycastHit hitInfo, 100))
{
collider = hitInfo.collider;//获取射线检测到的这个点的Collider组件
//如果射线检测到的,鼠标点的这个玩意是「地形」的话,移动到这个位置
if(collider.CompareTag("Terrain"))
{
foreach(Actor actor in selectedActors)
{
actor.StopTask();
Ray _ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if(Physics.Raycast(_ray, out RaycastHit raycast, 100, mouseDragLayerMask))
{
Vector3 _targetPos = raycast.point;
actor.SetDestination(_targetPos);
}
}
}
//如果射线检测到的,鼠标点的这个玩意是「石头/敌人/马车」的话,采集/攻击
else if(!collider.CompareTag("Player"))
{
Damageable damageable = collider.GetComponent<Damageable>();//获取到点击的这个含collider组件的游戏对象
if(damageable != null)
{
foreach(Actor actor in selectedActors)
{
actor.AttackTarget(damageable);//满足条件的选中角色,向鼠标点击的这个含Damageable脚本的游戏对象,调用AttackTarget方法
}
}
}
}
}
private void SelectActors()
{
DeselectActors();//每次框选其实都是重新选择,所以需要删去上一次的所有选中的角色
//Debug.Log("We have Selected Actors!!!!!!");
//dragSize = new Vector3(Mathf.Abs(dragSize.x), 1f, Mathf.Abs(dragSize.z / 2));//这里是Z在3D世界
dragSize.Set(Mathf.Abs(dragSize.x / 2), 1f, Mathf.Abs(dragSize.z / 2));//HalfExtent
RaycastHit[] hits = Physics.BoxCastAll(dragCenter, dragSize, Vector3.up, Quaternion.identity, 0, dragSelectedLayerMask);
foreach(RaycastHit hit in hits)
{
Actor actor = hit.collider.GetComponent<Actor>();//我们要检测,框选到的是不是Actor角色
if(actor != null)
{
selectedActors.Add(actor);//将选中的角色,添加到selectedActor这个List中
actor.visualHandler.Select();
}
}
}
private void DeselectActors()
{
foreach(Actor actor in selectedActors)
{
actor.visualHandler.Deselect();//将之前所有已经选中的Actors的下标光圈关闭
}
selectedActors.Clear();//Clear删除之前SelectedActors这个List中的所有元素
}
}
ActorVisualHandler
using DG.Tweening;
using UnityEngine;
public class ActorVisualHandler : MonoBehaviour
{
public SpriteRenderer spriteRenderer;
public void Select()
{
spriteRenderer.enabled = true; //开启
spriteRenderer.transform.DOScale(0, 0.35f).From().SetEase(Ease.OutBack);
}
public void Deselect()
{
spriteRenderer.enabled = false; //SR组件关闭
}
}
AnimationEventListener
using UnityEngine;
using UnityEngine.Events;
public class AnimationEventListener : MonoBehaviour
{
[HideInInspector] public UnityEvent footStepEvent = new UnityEvent();
[HideInInspector] public UnityEvent attackEvent = new UnityEvent();
public void FootstepEvent()
{
footStepEvent.Invoke();
}
public void AttackEvent()
{
attackEvent.Invoke();
}
}