Unity3D official case to achieve a red alert-like movement

Actor

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();
    }
}

CameraController

using UnityEngine;

public class CameraController : MonoBehaviour
{
    [SerializeField] Vector2 zoomLimits;
    [SerializeField] float cameraSpeed;
    Transform mainCamera;
    Transform zoomObject;
    Vector2 input;
    bool mouseMove = true;
    Vector3 moveInput;

    void Awake()
    {
        mainCamera = Camera.main.transform;
        transform.LookAt(mainCamera);
        zoomObject = transform.GetChild(0);
    }

    void Update()
    {
        moveInput.Set(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0);

        if (mouseMove)
        {
            Vector2 mousePos = Input.mousePosition;

            if (mousePos.x > Screen.width * 0.95f && mousePos.x < Screen.width)
                moveInput.x = 1;
            else if (mousePos.x < Screen.width * 0.05f && mousePos.x > 0)
                moveInput.x = -1;

            if (mousePos.y > Screen.height * 0.95f && mousePos.y < Screen.height)
                moveInput.z = 1;
            else if (mousePos.y < Screen.height * 0.05f && mousePos.y > 0)
                moveInput.z = -1;
        }

        Vector3 movementDirection = mainCamera.TransformDirection(moveInput);
        movementDirection.y = 0;
        transform.position += movementDirection.normalized * Time.deltaTime * cameraSpeed;

        zoomObject.localPosition += new Vector3(0, 0, -Input.mouseScrollDelta.y);
        zoomObject.localPosition = new Vector3(zoomObject.localPosition.x, zoomObject.localPosition.y,
            Mathf.Clamp(zoomObject.localPosition.z, zoomLimits.x, zoomLimits.y));
    }
}

Damageable

using System;
using UnityEngine;

//通常情况下,这里我会使用接口
//这个案例中这个脚本会涉及字段、事件、方法,所以使用简单的类
public class Damageable : MonoBehaviour
{
    [SerializeField] private int maxHp = 100;
    private int currentHp;

    //不去使用案例中的UnityEvent
    public event Action OnHit; //只需要+=添加事件就行了
    public event Action OnDestroy;

    private void Start()
    {
        currentHp = maxHp;
    }

    public void Hit(int _damageAmount)
    {
        OnHit(); //OnHit.Invoke();
        currentHp -= _damageAmount;
        if (currentHp <= 0)
        {
            OnDestroy(); //调用事件
            Destroy(gameObject);
        }
    }
}

DestroyVisual

using UnityEngine;

public class DestroyVisual : MonoBehaviour
{
    private Damageable damageable;
    public GameObject destroyParticle;

    private void Start()
    {
        damageable = GetComponent<Damageable>();
        damageable.OnDestroy += DestroyEffect; //事件的添加
    }

    private void OnDestroy()
    {
        damageable.OnDestroy -= DestroyEffect; //事件的取消订阅,防止内存泄漏
    }

    public void DestroyEffect()
    {
        Instantiate(destroyParticle, transform.position + Vector3.up, Quaternion.identity);
    }
}

Resources

using DG.Tweening;
using UnityEngine;

public enum Resourcetype
{
    Wood,
    Stone
}

public class Resources : MonoBehaviour
{
    [SerializeField] private Resourcetype resourceType;
    public bool isHovering; //判断采集物是否要高亮
    [SerializeField] private int bounsAmount;
    private MeshRenderer meshRenderer;
    private Color originalColor;

    private Damageable damageable;

    private void Start()
    {
        meshRenderer = GetComponent<MeshRenderer>();
        //originalColor = meshRenderer.material.color;//获取的其实只是材质的Base Color
        originalColor = meshRenderer.material.GetColor("_EmissionColor");

        damageable = GetComponent<Damageable>();
        damageable.OnHit += HitResources;
        damageable.OnDestroy += AddResources;
    }

    private void OnDestroy()
    {
        damageable.OnHit -= HitResources;
        damageable.OnDestroy -= AddResources; //事件的取消订阅,为了防止出现内存泄漏的问题!
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
            HitResources(); //仅做测试使用
    }

    //这个方法将会+=到OnHit事件上,表示当击中目标后,调用的方法
    public void HitResources()
    {
        //每次击中后,调用一个小动画
        transform.DOComplete();
        transform.DOShakeScale(0.5f, 0.2f, 10, 90, true);
    }

    //这个方法将会+=到OnDestroy事件上,表示目标阵亡后,调用这个方法
    public void AddResources()
    {
        Debug.Log("Resources : " + resourceType + " increase : " + bounsAmount + " !");
    }

    private void OnMouseEnter() //含Collider
    {
        isHovering = true;
        meshRenderer.material.SetColor("_EmissionColor", Color.gray);
    }

    private void OnMouseExit()
    {
        isHovering = false;
        meshRenderer.material.SetColor("_EmissionColor", originalColor);
    }
}

Guess you like

Origin blog.csdn.net/qq_36382679/article/details/114339444