[Unity Actual Combat] Make an Age of Empires, Red Police - RTS strategic game

Article Directory

Let's take a look at the final effect

insert image description here

What is an RTS game

When referring to RTS (i.e. Real Time Strategy) games, games of this genre usually emphasize the player's ability to make strategic and tactical decisions in a real-time environment. These games require players to simultaneously manage resources, build bases, recruit and command armies in order to achieve the goal of defeating the enemy. Here are a few well-known RTS games and their examples:

1. " 星际争霸II" (StarCraft II): This is a classic RTS game series developed by Blizzard Entertainment. Players can choose from three different races (Humans, Protoss, and Zerg) and fight in a futuristic universe. The game is known for its complex strategy and rich variety of game mechanics, including resource management, building construction, skill upgrading and tactical command.

2. " 红色警戒" series (Command & Conquer): This is a series of RTS games developed by Westwood. Players can act as commanders of global forces, participate in strategies and campaigns, control armies, build bases, research technologies and use various weapons to achieve victory. There are many different games in this series, such as Red Alert 2 and Red Alert 3.

3. " 帝国时代Age of Empires" series (Age of Empires): This is an RTS game series developed by Microsoft Game Studios. The series is based on the historical background, and players can choose different civilizations (such as Greece, Rome, and Mongolia), develop cities, build buildings, train armies, and conduct wars and trades. Age of Empires II and Age of Empires III are two popular entries in the series.

4. " 战争三国志Total War: Three Kingdoms" series (Total War: Three Kingdoms): This is an RTS game series developed by Creative Assembly, set in the Three Kingdoms period of Chinese history. Players can play different generals to unify China, expand territory, and carry out various political and military strategies. The series is known for its large-scale warfare and complex strategy system.

These are just a few examples in the field of RTS games. There are many other excellent games, such as "Age of Empires II: Conqueror", "Warcraft III" and "Incorporated Heroes", each of which has its own unique characteristics and gameplay.

One or two methods to realize camera movement + rotation + zoom and drag function

foreword

Standard real-time strategy games will have many elements such as resource collection and base construction. Players can command and control independent units, or group-style control, and use their talents as war leaders in the game, which is different from the first One-person main perspective, and over-the-shoulder camera, the camera in RTS games is more "free", and the visible map range and zoom level are also different. Many controls of the camera need to meet the multifunctional requirements of [ ] and [ ] at the same 键盘time 鼠标.

Two ways to realize camera control
第一种: Control the camera through keyboard input
第二种: the main camera is a sub-object, and the camera is controlled by Raycast and offset such as key input and mouse drag

Prepare

In order to save trouble, we directly download the official case and modify it, which saves the secondary important cumbersome steps such as setting up the environment. Here we mainly discuss the camera control of making RTS games

【Baidu Cloud】Link: https://pan.baidu.com/s/1LMT-n4SqCIGTscDKpyjCDA Password: te3j

Delete the original control of camera movement and scaling, and [Chinemachine Brain component] removal, we have to rewrite this part
insert image description here

the first way

1. to move

What we want to achieve is 键盘to control the movement of the camera through 【 】, and use the mouse to touch the edge of the screen to move the camera

1.1 Code implementation, detailed Chinese notes are written in it, so I won’t explain too much

using UnityEngine;

public class CameraController01 : MonoBehaviour
{
    
    
    private Vector3 moveInput;//接收键盘的输入量
    [SerializeField] private float panSpeed;//相机平移的速度

    [SerializeField] private float scrollSpeed;//鼠标滚动的速度

    private void Update()
    {
    
    
        HandleMovementInput();
    }

    private void HandleMovementInput()
    {
    
    
        //我们其实动态改变的是Main Camera的Trans组件的Pos
        Vector3 pos = transform.position;
		
		//接收键盘输入量
        //moveInput = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));//考虑性能,不要有太多的new
        moveInput.Set(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));

		//接收鼠标输入量,实现触屏幕边缘移动
        Vector2 mousePos = Input.mousePosition;
        if (mousePos.x > Screen.width * 0.9f && mousePos.x < Screen.width)
            moveInput.x = 1;
        if (mousePos.x < Screen.width * 0.1f && mousePos.x > 0)
            moveInput.x = -1;
        if (mousePos.y > Screen.height * 0.9 && mousePos.y < Screen.height)
            moveInput.z = 1;
        if (mousePos.y < Screen.height * 0.1 && mousePos.y > 0)
            moveInput.z = -1;

		//相机平移
        pos.x += moveInput.x * panSpeed * Time.deltaTime;
        pos.z += moveInput.z * panSpeed * Time.deltaTime;
        transform.position = pos;
    }
}

1.2 Effect:

insert image description here

1.3 Questions:

If you want to pan the camera at a constant speed completely through panSpeed, you should normalize/vectorize/vectorize movelnput. After our vector is "normalized", we can ensure that the camera's movement is completely controlled by passing panSpeed. Because in this example we use GetAxisRaw, GetAxisRawthe returned value can only be 0, -1 or 1, but if you use it GetAxis, then in Updatethe method, in fact, the increment of your os per frame is not A fixed value, the advantage of "normalization" is that it will movelnputensure that the modulus length of the magnitude (Magnitude) vector is 1

In fact, we only want to get moveInputthe direction of this vector, and don't care about its size, so 1*N=N, so we only want to get its direction, so that we can control it at a uniform speed panSpeedcompletely

Revise

//相机平移
pos.x += moveInput.normalized.x * panSpeed * Time.deltaTime;
pos.z += moveInput.normalized.z * panSpeed * Time.deltaTime;

2. Zoom

Next, let's quickly implement the zoom function of the camera. The zoom of the camera actually changes the value of the camera's Y axis, and what needs to be changed is the y-axis component in the variable pos. So what is the y-axis component in our pos
? In fact, it is our mouse [the input amount of the scroll wheel Input.GetAxis("Mouse ScrollWheel'")] to multiply by scrollSpeed, and then multiply byTime.deltaTime

2.1 Code

pos.x += moveInput.normalized.x * panSpeed * Time.deltaTime;
pos.y += Input.GetAxis("Mouse ScrollWheel") * scrollSpeed * Time.deltaTime;//Y轴滚轮输入量,实现缩放
pos.z += moveInput.normalized.z * panSpeed * Time.deltaTime;

3. Restrictions

When we zoom out to a certain extent, we can continue to zoom out, and the camera can be panned in an infinite range, and it can move out of the scene

Here we can use a method called [ Mathf.Clampmethod] to limit, Clampthe original meaning can be expressed as a clip, imagine that the first parameter is clamped within the range of the second parameter and the third parameter

the code

pos.x = Mathf.Clamp(pos.x, -10, 10);
pos.y = Mathf.Clamp(pos.y, 5, 30);
pos.z = Mathf.Clamp(pos.z, -25,5);//根据自己的地图范围进行调整,可以设置为变量,方便修改

4. Complete code

Well, this is the first type of "basic version" camera movement, which is mainly triggered by [keyboard input] and mouse [close to the edge of the screen] to trigger camera movement, including zooming of the camera wheel and imposing a limit range.

using UnityEngine;

public class CameraController01 : MonoBehaviour
{
    
    
    private Vector3 moveInput;//接收键盘的输入量
    [SerializeField] private float panSpeed;//相机平移的速度

    [SerializeField] private float scrollSpeed;//鼠标滚动的速度

    private void Update()
    {
    
    
        HandleMovementInput();
    }

    private void HandleMovementInput()
    {
    
    
        //我们其实动态改变的是Main Camera的Trans组件的Pos
        Vector3 pos = transform.position;

        //moveInput = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));//性能?
        moveInput.Set(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));

        Vector2 mousePos = Input.mousePosition;
        if (mousePos.x > Screen.width * 0.9f && mousePos.x < Screen.width)
            moveInput.x = 1;
        if (mousePos.x < Screen.width * 0.1f && mousePos.x > 0)
            moveInput.x = -1;
        if (mousePos.y > Screen.height * 0.9 && mousePos.y < Screen.height)
            moveInput.z = 1;
        if (mousePos.y < Screen.height * 0.1 && mousePos.y > 0)
            moveInput.z = -1;

        pos.x += moveInput.normalized.x * panSpeed * Time.deltaTime;
        pos.y += Input.GetAxis("Mouse ScrollWheel") * scrollSpeed * Time.deltaTime;//Y轴滚轮输入量
        pos.z += moveInput.normalized.z * panSpeed * Time.deltaTime;

        pos.x = Mathf.Clamp(pos.x, -10, 10);
        pos.y = Mathf.Clamp(pos.y, 5, 30);
        pos.z = Mathf.Clamp(pos.z, -25, 5);//根据自己的地图范围进行调整,可以设置为变量,方便嘛!

        transform.position = pos;
    }
}

the second way

Our second method can be realized by controlling the movement of [ ], so that the camera of the sub-object can be moved and rotated 父物体Camera Rigwith [ ] indirectly.父物体

1. Mobile Code

1.1 Code, use Lerp interpolation function to achieve moving buffer effect

if (Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.W))
	newPos += transform.forward * panSpeed * Time.deltaTime;//相机平移向上
if (Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.S))
    newPos -= transform.forward * panSpeed * Time.deltaTime;
if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D))
    newPos += transform.right * panSpeed * Time.deltaTime;//相机平移向右
if(Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A))
    newPos -= transform.right * panSpeed * Time.deltaTime;
    
//transform.position = newPos;//AxisRaw / Axis
//Lerp方法:当前位置,目标位置,最大距离:速度 * 时间 =>从当前位置,到目标位置,需要多少时间到达
transform.position = Vector3.Lerp(transform.position, newPos, moveTime * Time.deltaTime);

1.2 Press Shift to accelerate

If you think that the panning speed is very slow, you want to press shift to speed up the panning of the camera, we can set two different panning speeds, when we press the shift button, assignpanSpeed = fastSpeed;

[SerializeField] private float normalSpeed, fastSpeed;

if (Input.GetKey(KeyCode.LeftShift))
	panSpeed = fastSpeed;
else
    panSpeed = normalSpeed;

2. Rotate

In fact, what we need to change is the value of rotation Y on the Camera Rig game object

insert image description here
Code

private Vector3 newPos;
private Quaternion newRotation;
[SerializeField] private float rotationAmount;//旋转的程度

private void Start()
{
    
    
	newPos = transform.position;
    newRotation = transform.rotation;
}
private void Update()
{
    
    
	if (Input.GetKey(KeyCode.Q))
        newRotation *= Quaternion.Euler(Vector3.up * rotationAmount);//Q:逆时针
    if (Input.GetKey(KeyCode.E))
        newRotation *= Quaternion.Euler(Vector3.down * rotationAmount);//(0,-1,0)顺时针

	//transform.position = newPos;//AxisRaw / Axis
    //Lerp方法:当前位置,目标位置,最大距离:速度 * 时间 =>从当前位置,到目标位置,需要多少时间到达
    transform.position = Vector3.Lerp(transform.position, newPos, moveTime * Time.deltaTime);

    //transform.rotation = newRotation;
    transform.rotation = Quaternion.Lerp(transform.rotation, newRotation, moveTime * Time.deltaTime);
}

3. Zoom

If you want to zoom with the center point of the camera, here is another way of thinking: click on the sub-object Main Camera, select Localthe local coordinate system, and under its own coordinate system, we can drag the z-axis, which is actually in the Main Camera. The values ​​of z and its y have changed

It should be noted here that because the Main Camera belongs to the child object, the value in the position of the Trans component changed here actually belongs to LocalPosition, and if an object is used as the parent object, then the value in the Trans component of the game object The position belongs to the world coordinate system

That is what I just said, in the Sart method, the initial zoom should be the current position of the sub-object camera localPosition, not the origin of the empty object Camera Rig. All the zoom operations here are based on the localPosition in the sub-object Main Camera. renew

private Transform cameraTrans;//子物体~主相机Trans,要改YZ数值
[SerializeField] private Vector3 zoomAmount;//要改YZ数值,设置zoomAmount结构体中YZ的数值
private Vector3 newZoom;

private void Start()
{
    
    
    cameraTrans = transform.GetChild(0);
    newZoom = cameraTrans.localPosition;
}

private void Update()
{
    
    
	if (Input.GetKey(KeyCode.R))
        newZoom += zoomAmount;//放大功能:Y越来越小,Z越来越大
    if (Input.GetKey(KeyCode.F))
        newZoom -= zoomAmount;//缩小:Y越来越大,Z越来越小
            
	cameraTrans.localPosition = Vector3.Lerp(cameraTrans.localPosition, newZoom, moveTime * Time.deltaTime);
}

If you want to zoom in and out to a limited range, you can refer to the Clamp method just now, so I won’t repeat it too much here

4. Mouse operation

If you want to drag the map with the mouse to operate the panning, rotating, and zooming of the camera

4.1 Mouse drag and pan code

private Vector3 dragStartPos, dragCurrentPos;//鼠标拖拽的起始点,和鼠标拖拽的当前位置
private Vector3 rotateStart, rotateCurrent;//鼠标初始位置和当前位置,用来计算相机旋转角度

if(Input.GetMouseButtonDown(1))//鼠标按下一瞬间!
{
    
    
    Plane plane = new Plane(Vector3.up, Vector3.zero);
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    float distance;
    if(plane.Raycast(ray, out distance))//out输出参数,一般方法返回一个数值,out则返回return和out数值,2个结果
    {
    
    
        dragStartPos = ray.GetPoint(distance);
    }
}

if (Input.GetMouseButton(1))//鼠标按着(当前)
{
    
    
    Plane plane = new Plane(Vector3.up, Vector3.zero);
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    float distance;
    if (plane.Raycast(ray, out distance))//out输出参数,一般方法返回一个数值,out则返回return和out数值,2个结果
    {
    
    
        dragCurrentPos = ray.GetPoint(distance);

        Vector3 difference = dragStartPos - dragCurrentPos;//大家可以试试反过来写,效果雷同只是方向相反
        newPos = transform.position + difference;
    }
}

4.2 Mouse wheel zoom

newZoom += Input.mouseScrollDelta.y * zoomAmount;

4.3 Rotation

We hope that by dragging the [middle button] of the mouse, we can achieve the effect of the original keyboard rotation of the camera

if (Input.GetMouseButtonDown(2))
	rotateStart = Input.mousePosition;
if(Input.GetMouseButton(2))
{
    
    
    rotateCurrent = Input.mousePosition;
    Vector3 difference = rotateStart - rotateCurrent;

    rotateStart = rotateCurrent;//赋值最新的鼠标位置

	//x y控制水平还是垂直方向拖动控制的旋转	
    newRotation *= Quaternion.Euler(Vector3.up * -difference.x / 20);//水平方向触发旋转
    //newRotation *= Quaternion.Euler(Vector3.up * -difference.y / 20);//垂直方向
}

5. Complete code

using System;
using UnityEngine;

public class CameraController02 : MonoBehaviour
{
    
    
    private float panSpeed;
    [SerializeField] private float moveTime;//缓冲时间,用于之后的Vector3.Lerp和Quaternion.Lerp方法/函数
    [SerializeField] private float normalSpeed, fastSpeed;

    private Vector3 newPos;
    private Quaternion newRotation;
    [SerializeField] private float rotationAmount;//旋转的程度

    private Transform cameraTrans;//子物体嘛~主相机Trans,要改YZ数值
    [SerializeField] private Vector3 zoomAmount;//要改YZ数值,设置zoomAmount结构体中YZ的数值
    private Vector3 newZoom;

    private Vector3 dragStartPos, dragCurrentPos;//鼠标拖拽的起始点,和鼠标拖拽的当前位置
    private Vector3 rotateStart, rotateCurrent;//鼠标初始位置和当前位置,用来计算相机旋转角度

    private void Start()
    {
    
    
        newPos = transform.position;
        newRotation = transform.rotation;

        cameraTrans = transform.GetChild(0);
        newZoom = cameraTrans.localPosition;
    }

    private void Update()
    {
    
    
        HandleMovementInput();//通过键盘控制相机
        HandleMouseInput();//通过鼠标控制相机
    }

    private void HandleMouseInput()
    {
    
    
        if(Input.GetMouseButtonDown(1))//鼠标按下一瞬间!
        {
    
    
            Plane plane = new Plane(Vector3.up, Vector3.zero);
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            float distance;
            if(plane.Raycast(ray, out distance))//out输出参数,一般方法返回一个数值,out则返回return和out数值,2个结果
            {
    
    
                dragStartPos = ray.GetPoint(distance);
            }
        }

        if (Input.GetMouseButton(1))//鼠标按着(当前)
        {
    
    
            Plane plane = new Plane(Vector3.up, Vector3.zero);
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            float distance;
            if (plane.Raycast(ray, out distance))//out输出参数,一般方法返回一个数值,out则返回return和out数值,2个结果
            {
    
    
                dragCurrentPos = ray.GetPoint(distance);

                Vector3 difference = dragStartPos - dragCurrentPos;//大家可以试试反过来写,效果雷同只是方向相反
                newPos = transform.position + difference;
            }
        }

        newZoom += Input.mouseScrollDelta.y * zoomAmount;

        if (Input.GetMouseButtonDown(2))
            rotateStart = Input.mousePosition;
        if(Input.GetMouseButton(2))
        {
    
    
            rotateCurrent = Input.mousePosition;
            Vector3 difference = rotateStart - rotateCurrent;

            rotateStart = rotateCurrent;//赋值最新的鼠标位置
            newRotation *= Quaternion.Euler(Vector3.up * -difference.x / 20);//水平方向触发旋转
            //newRotation *= Quaternion.Euler(Vector3.up * -difference.y / 20);//垂直方向
        }
    }

    private void HandleMovementInput()
    {
    
    
        if (Input.GetKey(KeyCode.LeftShift))
            panSpeed = fastSpeed;
        else
            panSpeed = normalSpeed;

        if (Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.W))
            newPos += transform.forward * panSpeed * Time.deltaTime;//相机平移向上
        if (Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.S))
            newPos -= transform.forward * panSpeed * Time.deltaTime;
        if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D))
            newPos += transform.right * panSpeed * Time.deltaTime;//相机平移向右
        if(Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A))
            newPos -= transform.right * panSpeed * Time.deltaTime;

        if (Input.GetKey(KeyCode.Q))
            newRotation *= Quaternion.Euler(Vector3.up * rotationAmount);//Q:逆时针
        if (Input.GetKey(KeyCode.E))
            newRotation *= Quaternion.Euler(Vector3.down * rotationAmount);//(0,-1,0)顺时针

        if (Input.GetKey(KeyCode.R))
            newZoom += zoomAmount;//放大功能:Y越来越小,Z越来越大
        if (Input.GetKey(KeyCode.F))
            newZoom -= zoomAmount;//缩小:Y越来越大,Z越来越小

        //transform.position = newPos;//AxisRaw / Axis
        //Lerp方法:当前位置,目标位置,最大距离:速度 * 时间 =>从当前位置,到目标位置,需要多少时间到达
        transform.position = Vector3.Lerp(transform.position, newPos, moveTime * Time.deltaTime);

        //transform.rotation = newRotation;
        transform.rotation = Quaternion.Lerp(transform.rotation, newRotation, moveTime * Time.deltaTime);

        cameraTrans.localPosition = Vector3.Lerp(cameraTrans.localPosition, newZoom, moveTime * Time.deltaTime);
    }
}

2. The enlarged animation of the box-shaped frame selection character and the aperture at the bottom of the character

Here is no detailed environment construction and character animation implementation. We directly download the official demo of Unity to modify and learn. You can directly download the "Project Start Edition" to start learning. Many original functions are deleted in it, and we will redevelop it.

[Project start version] https://pan.baidu.com/s/17JFX1ihjQdWQn9iFqVkGGw Password: up15

1. First of all, we hope that the base position of the character selected in the box will have an obvious aperture display

insert image description here

1.1 Here we create an empty object for each character: Selected Sprite, and then add the SpriteRenderer component

insert image description here
insert image description here

1.2 Select texture

insert image description here

1.3 Adjust the angle and size

insert image description here

1.4 Adjust Brightness

If you feel that the brightness of the circle is too dark, we can create a new material and control the brightness by controlling the color of the material to be transparent. The most convenient method is " ", which is called, and remember to set the 万金油SpriteRenderer shadercomponent Legacy Shaders/Paticles/Additivefirst 禁用, because the default role is not selected.
insert image description here

2. Next, let's make a visualization, a transparent range selected by the box box that is visible after dragging

2.1 Here I created a 3D cube for display, that is cube

After a while, we will drag and drop the mouse, change the value of its LocalScale by changing the position of the mouse, change the selection range of the box, and present the corresponding shape and size
insert image description here

2.2 Create a new material to control the transparency of the cube

insert image description here

2.3 The code of the selected circle on the bottom of the character's foot

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;

public class ActorVisualHandler : MonoBehaviour
{
    
    
    public SpriteRenderer spriteRenderer;

    public void Select()
    {
    
    
        spriteRenderer.enabled = true;//开启
        //使用DOTween实现动画效果
        spriteRenderer.transform.DOScale(0, 0.35f).From().SetEase(Ease.OutBack);

    }

    public void Deselect()
    {
    
    
        spriteRenderer.enabled = false;//SR组件关闭
    }

}

2.3 ActorManager code

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)
                {
    
    
                    Ray _ray = Camera.main.ScreenPointToRay(Input.mousePosition);
                    if(Physics.Raycast(_ray, out RaycastHit raycast, 100, mouseDragLayerMask))
                    {
    
    
                        Vector3 _targetPos = raycast.point;
                        actor.SetDestination(_targetPos);
                    }
                }
            }
            //如果射线检测到的,鼠标点的这个玩意是「石头/敌人/马车」的话,采集/攻击
        }
    }

    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中的所有元素
    }

}

mount script
insert image description here

3. The problem of UI occlusion

We can use it EventSystem.current.IsPointerOverGameObject(). This method will detect whether the click is on the UI. If it happens to be on the UI when dragging, then we might as well stop the dragging immediately; and the isDraggingi=falsebox selection cube is invisible, and directly return, after not executing all content

the code

if(UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameobject())
{
    
    
	isDragging false;
	selectedArea.gameobject.SetActive(false);
	return;
}

3. The movement and collection of characters, the highlighting of the mouse and objects, and DOTween animation solve the problem of repeated pressing deformities

1. to move

1.1 Navigation Baking

First, bake the navigation scene map. If you don’t understand it, you can read my previous article: Navigation System
insert image description here

1.2 Character movement code

Actor code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class Actor : MonoBehaviour
{
    
    
    [HideInInspector] public ActorVisualHandler visualHandler;
    private NavMeshAgent agent;
    private Animator animator;

    private void Start()
    {
    
    
        visualHandler = GetComponent<ActorVisualHandler>();
        agent = GetComponent<NavMeshAgent>();
        animator = GetComponentInChildren<Animator>();
    }

    private void Update()
    {
    
    
        //Animator是通过人物的速度Speed来切换不同的动画状态片段的
        animator.SetFloat("Speed", Mathf.Clamp(agent.velocity.magnitude, 0, 1));
    }

    //人物的移动, _target表示人物应该移动到的鼠标位置,由于是未知的,所以设置为参数
    public void SetDestination(Vector3 _target)
    {
    
    
        agent.SetDestination(_target);
    }

}

1.3 ActorManager code

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.SetDestination(hitInfo.point);
             }
         }
         //如果射线检测到的,鼠标点的这个玩意是「石头/敌人/马车」的话,采集/攻击
     }
 }

Here is a section, some people may already be familiar with mobile, and mobile is also a relatively routine operation, so here is a project start version for download: https://pan.baidu.com/s/1oI4n4KCSRUox4goPqtDdygPassword
: b1fk

2. Highlight

When we put the mouse on the game object, we want to achieve 高亮the effect, and different resource categories need to be divided

How to achieve highlighting? In fact, it essentially changes the HDR Color value of the stone material to achieve a highlight effect.
insert image description here
insert image description here
Create a C# script: Resources. We first add the script to two Prefabs. The code is implemented as follows:

using DG.Tweening;

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 void Start()
    {
    
    
        meshRenderer = GetComponent<MeshRenderer>();
        //originalColor = meshRenderer.material.color;//获取的其实只是材质的Base Color
        originalColor = meshRenderer.material.GetColor("_EmissionColor");
    }
    
	private void OnMouseEnter()//含Collider
    {
    
    
        isHovering = true;
        meshRenderer.material.SetColor("_EmissionColor", Color.gray);
    }

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

3. Gathering and animation

Here we will deal with a very common problem in the game, that is, the influence of 攻击力the character on the collection (enemy)生命值

3.1 We create a Damageablel script

using System;

//通常情况下,这里我会使用接口
//这个案例中这个脚本会涉及字段、事件、方法,所以使用简单的类
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);
        }
    }
}

3.2 Call in Resources

private Damageable damageable;

private void Start()
{
    
    
    damageable = GetComponent<Damageable>();
}

We continue to write the HitResources method, which will be added to the OnHit event. If the collection is hit, the OnHit event will be triggered, and the HitResources method added to the OnHit event will be called. The same is true below, just added The event becomes the OnDestroy event

For example, if we want to give it a simple animation, we can introduce the DG.Tweening namespace

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

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

Subscribe and cancel events

private void Start()
{
    
    
     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();//仅做测试使用
}

4. Distortion

If we press several times in a row, we will find that the same object has different shape distortions after the animation is completed. For some carriages, you will see that it is compressed and deformed. The reason is that we press
continuously When the Space button is pressed to call the animation, we proceed to the next animation immediately after the previous animation has not been completed, resulting in a deformed change in size ( 形状不统一是因为随机种子在影响,形状未还原是因为动画未完成又再次进行)

insert image description here
solve

That is to call DOTween APIone of it called: transform.DOComplete();on it

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

5. Collection

That is, when the mouse clicks on the stone, the character starts to move nearby, and starts collecting and playing the attack animation

Add keyframe event
insert image description here
Actor code at the appropriate position for the attack animation

using System;
using System.Collections;
using System.Collections.Generic;
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;

    private void Start()
    {
    
    
        visualHandler = GetComponent<ActorVisualHandler>();
        agent = GetComponent<NavMeshAgent>();
        animator = GetComponentInChildren<Animator>();

        animationEvent = GetComponentInChildren<AnimationEventListener>();
        animationEvent.attackEvent.AddListener(Attack);//将Attack作为事件,在动画帧上执行
    }

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

    private void StopTask()
    {
    
    
        damageableTarget = null;//首先,让采集目标为空
        if (currentTask != null)//停止正在手头的这份工作,即:立即停止协程的执行
            StopCoroutine(currentTask);
    }
}

Called in ActorManager.cs

private void SetTask()
{
    
    
    //首先要注意一点,如果我们框选空气的话,或者随便点击左键,触发这个方法都不该有什么逻辑执行
    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"))
        {
    
    
            //...
        }
        //如果射线检测到的,鼠标点的这个玩意是「石头/敌人/马车」的话,采集/攻击
        else if(!collider.CompareTag("Player"))
        {
    
    
            Damageable damageable = collider.GetComponent<Damageable>();//获取到点击的这个含collider组件的游戏对象
            if(damageable != null)
            {
    
    
                foreach(Actor actor in selectedActors)
                {
    
    
                    actor.AttackTarget(damageable);//满足条件的选中角色,向鼠标点击的这个含Damageable脚本的游戏对象,调用AttackTarget方法
                }
            }
        }
    }
}

6. Add particle effects

"Feeling warm and thinking about lust", we started to add a little particle effect to enrich the interaction with this scene

6.1 Implementation of UnityEvent event method

Modify the Actor.cs code

public GameObject hitEffect;

public void AttackEffect()
{
    
    
    if (damageableTarget)
        Instantiate(hitEffect, damageableTarget.transform.position, Quaternion.identity);
}

Add events in front of Actor.cs

private void Start()
{
    
    
    animationEvent.attackEvent.AddListener(AttackEffect);
}

Drag in Particle Effects
insert image description here
Effect
insert image description here

6.2 c# event method implementation

We just used the UnityEvent event to complete it. Now, let’s use the c# event to take a look. For example, when it is destroyed, we also give it an effect. Here we create a new script: , the DestroyVisualtwo type of collection, added to this script

Since our event is in the Damageable script, our subsequent methods must also be added to the event of this script, so we declare and obtain the corresponding reference, declare the destroyed particle effect, and create a method here as the event responder Event handler, simple Instantiate

using System.Collections;
using System.Collections.Generic;
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);
    }

}

Drag in the destroy particle effect
insert image description here
effect
insert image description here

final code

Full version: https://pan.baidu.com/s/1alZejJUkcbwvdYJpdGxq-A Password: s412

reference

[Unity official short video] https://www.bilibili.com/video/BV1fy4y1J7Es
[Video]: https://www.bilibili.com/video/BV1zK4y1Q74m/?spm_id_from=333.999.0.0&vd_source=2526a18398a079ddb95468a0c73f126e
【 Video]: https://www.bilibili.com/video/BV1wz4y1m7LM/?spm_id_from=333.999.0.0&vd_source=2526a18398a079ddb95468a0c73f126e
[Video]: https://www.bilibili.com/video/BV1jV411v74U/?spm_id_ from=333.999.0.0&vd_source=2526a18398a079ddb95468a0c73f126e

end

If you have other better methods, you are welcome to comment and share them. Of course, if you find any problems or questions in the article, please also comment and private message me.

Well, I am Xiang Yu, https://xiangyu.blog.csdn.net/

A developer who worked silently in a small company, out of interest, began to study unity by himself. Recently, I created a new column [You Ask Me Answer], mainly to collect your questions. Sometimes a question may not be clear in a few words, and I will answer it in the form of an article. Although I may not necessarily know some problems, I will check the information of various parties and try to give the best advice. I hope to help more people who want to learn programming. Let's encourage each other~

Guess you like

Origin blog.csdn.net/qq_36303853/article/details/131425981