【Unity】使用Unity编写传统ARPG游戏人物操作方式(二)

(Ps:本文为个人原创旧文章,原发布地址为:CSDN
在上一篇博客中已经介绍过了3种ARPG游戏的人物操作方式的代码实现方法,在这篇博客里会紧接着给出另外三种操作方式,分别为:
(4) 经典3D开放角度操作方式,如日本SQUARE ENIX旗下的一款《最终幻想零式》游戏。
(5) 经典3D无锁定视角操作方式,我自己在写的一款游戏就是采用这种操作方式,这次正好用上了;
(6) 经典3D固定面向方向操作方式,如盛大旗下代理的一款韩国游戏《龙之谷》;
闲话不说,继续下一种操作方式的分析与实现方法。

(4)经典3D开放角度操作方式

提到日本SQUARE ENIX公司,几乎全世界的人应该都知道吧。嗯,这个曾开发过许多非常具有价值的游戏系列,其中包括:《最终幻想》系列游戏还有一款个人童年时期玩过的最喜欢的一款回合制RPG游戏《LIVE_A_LIVE》,可惜这款游戏并没有像《最终幻想》一样成为一个系列游戏,没玩过的同学可以尝试下载来玩一下,还是不错的。回归重点,这次要简单实现的是《最终幻想》系列中《最终幻想零式》的操作方式。由于该游戏最早是在PSP上发行的,但是我们是在Unity里面进行模拟,因此我们采用键盘的方式来实现。这种操作方式的特点是:安全脱离鼠标操作,视角转动和人物行走转向完全依赖于键盘按键的操作,并且人物行走角度也是开放的,无限制。同样采用CharacterController控件里的Move()方法进行移动的实现,3D游戏里基本都是用这个控件实现的。
同样给出简单的实现代码:

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(CharacterController))]
[RequireComponent(typeof(Animator))]
public class FF_Controller : MonoBehaviour {

    private CharacterController Controller;
    private Animator animator;

    private float V;
    private float H;
    private Vector3 DefaultDirection = Vector3.forward;
    private Vector3 MoveDirection;
    private float y;
    private float CurrentSpeed;
    //public Variable
    public float MoveSpeed;
    public float TurnSpeed;
    public float RotateSpeed;
    public float Gravity;
    void Awake()
    {
        Controller = this.GetComponent<CharacterController>();
        animator = this.GetComponent<Animator>();
    }

	// Use this for initialization
	void Start () 
    {
        V = 0;
        H = 0;
        DefaultDirection = Vector3.forward;

	}
	
	// Update is called once per frame
	void Update () 
    {
        V = Input.GetAxis("Vertical");
        H = Input.GetAxis("Horizontal");
	}

    void FixedUpdate()
    {
        Move();
    }

    void Move()
    {
        if (Controller.isGrounded)
        {
            MoveDirection = DefaultDirection;
            animator.SetFloat("V", V);
            animator.SetFloat("H", H);
            transform.RotateAround(transform.position, Vector3.up * H, RotateSpeed * Time.deltaTime);
            JudgeSpeed();
            MoveDirection = new Vector3(H, 0, V);
            MoveDirection = transform.TransformDirection(MoveDirection);
            MoveDirection *= CurrentSpeed;
            MoveDirection = new Vector3(MoveDirection.x, 0, MoveDirection.z);
            Controller.Move(MoveDirection * Time.deltaTime);  
        }
        else
        {
            y = MoveDirection.y - Gravity * Time.deltaTime;
        }
        
    }

    void JudgeSpeed()
    {
        AnimatorStateInfo AnimatorState = animator.GetCurrentAnimatorStateInfo(0);
        if (AnimatorState.IsName("Walk"))
        {
            CurrentSpeed = MoveSpeed;
        }
        else if(AnimatorState.IsName("TurnRight") || AnimatorState.IsName("TurnLeft"))
        {
            CurrentSpeed = TurnSpeed;
        }
    }
}

嗯,该怎么进行解释呢?sunset在这里使用了新版的Aniamtor系统进行的动画播放,新版的Animator系统相较旧版的动画系统实现起来更为直观,简单,也更容易在动画里添加事件,简单来说,就是简易实用,建议使用。还值得一提的是,包含人物的移动的代码实现的函数最好放在FixedUpdate()里,而对键盘输入事件的监听则可以放在Update()函数里。嗯,基础的也就差不多了。哦,对了针对这种方式,由于也是通过键盘进行转视角操作的,所以Sunset也写了一份Camera的代码实现摄像机的转视角操作。接下来是Camera的代码:

 /// <summary>
    /// //观察目标
    /// </summary>
    public Transform Target;
    /// <summary>
    /// 限定观察距离
    /// </summary>
    public float Distance = 10.0F;
    /// <summary>
    /// The speed x.
    /// </summary>
    ///X轴和Y轴的旋转速度
    private float SpeedX = 30;
    private float SpeedY = 30;
    /// <summary>
    /// 角度限制
    /// </summary>
    private float MinLimit_Y = 30;
    private float MaxLimit_Y = 180;
    /// <summary>
    ///旋转角度
    /// </summary>
    private float mX = 0.0F;
    private float mY = 0.0F;
    /// <summary>
    /// 经计算后,旋转到的角度,以便平滑旋转
    /// </summary>
    private Quaternion CRotation;
    private Vector3 CPosition;
    /// <summary>
    /// The temp_transform.
    /// </summary>
    private Transform Temp_transform;

    //public CharacterController collider;

    void Awake()
    {
        Temp_transform = transform;
    }

    void Start()
    {
        //初始化旋转角度
        mX = transform.eulerAngles.x;
        mY = transform.eulerAngles.y;
    }
    void LateUpdate()
    {
        if (Target != null)
        {
            if (Input.GetKey(KeyCode.E))
            {
                transform.RotateAround(this.transform.position, -1 * Vector3.up, SpeedX * Time.deltaTime);
            }
            else if (Input.GetKey(KeyCode.Q))
            {
                transform.RotateAround(this.transform.position, Vector3.up, SpeedX * Time.deltaTime);
            }
            if (Input.GetKey(KeyCode.R))
            {
               
                transform.RotateAround(this.transform.position, transform.right, SpeedY * Time.deltaTime);
            }
            else if(Input.GetKey(KeyCode.T))
            {
               
                transform.RotateAround(this.transform.position, -1 * transform.right, SpeedY * Time.deltaTime);
            }
           
            CRotation = this.transform.rotation;
            //重新计算位置
            CPosition = CPosition  * new Vector3(0.0F, 0.0F, -Distance) + Target.transform.position;
            transform.position = CPosition ;
        }

    }

Camera的旋转主要是利用transform.RotateAround(X, X, X)函数进行实现的,这个函数在API文档里也可以找到很详细的注解。嗯,继续下一种操作方式的介绍。

(5) 经典3D无锁定视角操作方式

sunset在个人制作的一款ARPG游戏里有使用到这种操作方式,相较于上述的其他操作方式也更为了解。这种操作方式的人物行走方式其实跟上一种操作方式之间区别是不大的,主要区别在于由鼠标控制视角的转动,并非完全依赖于键盘的操作。
同样使用Input.GetAxis()来获取Z方向和X方向上量的改变来进行移动,而视角的变化主要取决于鼠标的左右移动。
由于移动代码与上一种差不多就不在重复给出了,直接给出camera的代码:

	/// <summary>
	/// //观察目标
	/// </summary>
	public Transform Target;
	/// <summary>
	/// 观察距离
	/// </summary>
	public float Distance = 10.0F;
	/// <summary>
	/// The speed x.
	/// </summary>
	//旋转速度
	private float SpeedX=120;
	private float SpeedY=120;
	//角度限制
	private float  MinLimitY = 5;
	private float  MaxLimitY = 360;
	/// <summary>
	///旋转角度
	/// </summary>
	private float mX = 0.0F;
	private float mY = 0.0F;
	/// <summary>
	///鼠标缩放距离最值
	/// </summary>
	private float MaxDistance=15.0f;
	private float MinDistance=5.0f;
	/// <summary>
	///鼠标缩放速率
	/// </summary>
	private float ZoomSpeed=10f;
	/// <summary>
	///是否启用差值
	/// </summary>
	public bool isNeedDamping=true;
	/// <summary>
	///缩放速度
	/// </summary>
	public float Damping=20f;
	/// <summary>
	/// 经计算后,旋转到的角度,以便平滑旋转
	/// </summary>
    private Quaternion CRotation;
    private Vector3 CPosition;
	/// <summary>
	/// The temp_transform.
	/// </summary>
	private Transform Temp_transform ;

	private bool closer = false;
    public CursorSetting cursorSetting;


	void Awake()
	{
		Temp_transform = transform;
	}

	void Start () 
	{
		//初始化旋转角度
		mX=transform.eulerAngles.x;
		mY=transform.eulerAngles.y;
	}
	void LateUpdate () 
	{
        if (Target != null)
        {
                //获取鼠标输入
                mX += Input.GetAxis("Mouse X") * SpeedX * 0.02F;
                mY -= Input.GetAxis("Mouse Y") * SpeedY * 0.02F;
                //计算旋转
                CRotation = Quaternion.Euler(mY, mX, 0);
              
                //角度限制
                mY = LimitAngle(mY, MinLimitY, MaxLimitY);

                
                if (isNeedDamping)
                {
                    transform.rotation = Quaternion.Lerp(transform.rotation, CRotation, Time.deltaTime * Damping);
                }
                else
                {
                    transform.rotation = CRotation;
                }
                //处理同时按下鼠标右键和方向控制键
                //if(Target.GetComponent<NoLockiVew_Player>().State==NoLockiVew_Player.PlayerState.Walk){
                //	Target.rotation=Quaternion.Euler(new Vector3(0,mX,0));
                //}


                //鼠标滚轮缩放
                Distance -= Input.GetAxis("Mouse ScrollWheel") * ZoomSpeed;
                Distance = Mathf.Clamp(Distance, MinDistance, MaxDistance);

                //重新计算位置
                CPosition = CRotation * new Vector3(0.0F, 0.0F, -Distance) + Target.transform.position;

                //设置相机的角度和位置
                if (isNeedDamping)
                {
                    transform.position = Vector3.Lerp(transform.position, CPosition, Time.deltaTime * Damping);
                }
                else
                {
                    transform.position = CPosition;
                    
                }
            
        }
		
	}
	private float LimitAngle (float angle,float min,float max) 
	{
        if (angle < -360)
        {
            angle += 360;
        }
        if (angle > 360)
        {
            angle -= 360;
        }
        return Mathf.Clamp (angle, min, max);
	}

这里设置Camera的代码同样十分简单,主要是运用了Quaternion里面的几个方法,实现鼠标移动时,Camera的平滑转动。在制作3D游戏时Camera的转动平滑是十分重要的问题,转动速度过快可能会导致晕3D,转动不够平滑也会使效果变得拖拉,卡顿。这里只是一个Camera的示例代码,真正用于游戏中的Camera代码会比这个复杂许多,以后有机会的话再开一篇博客仔细研究。

(6) 经典3D固定面向方向操作方式

这种操作模式比较少见,最好的例子就是盛大代理的一款韩国游戏《龙之谷》。这款ARPG游戏是十分受人欢迎的,本人也在大一时玩过一段时间。《龙之谷》最突出的特点就是在战斗过程中我们都只能看见角色的背部,即角色总是固定面向Camera所朝向的方向,随着Camera的转动而转动。sunset一开始在想着实现这种操作方式的时候,遇到了让人比较心烦的问题,(可能是因为熬夜太晚,脑供血不足)睡了一觉后问题迎刃而解,思路自然而然的出来了。因为角色的Rotation是随着Camera的转动而改变的,所以必然需要两份脚本,一份人物的控制器脚本,一份Camera的控制脚本。Camera的控制脚本大可继续使用上文中的Camera设置的脚本,只要在其中加入这样一段代码:

ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
LookAtPoint = ray.GetPoint(20.0f);

即由Camera对应的视口位置向前射出一条穿过屏幕中心点的一条射线,同时使用ray.GetPoint(_Dist)方法获取范围_Dist外在射线上的一个点的Vector3量,保存这个量,设为公开的(public)。我这里的_Dist取20.0f,一般来说是足够的。
然后是角色的操作代码:

private float V;
    private float H;

    private CharacterController Controller;
    private Animator animator;
    private Vector3 MoveDirection;
    private Vector3 DefaultDirection;
    private float y;
    private float Gravity;

    private float CurrentSpeed;
    public float WalkSpeed;
    public float RunSpeed;
    public D_Camera d_Camera;
    
    void Awake()
    {
        animator = this.GetComponent<Animator>();
        Controller = this.GetComponent<CharacterController>();
    }
    void Update()
    {
        this.transform.LookAt(new Vector3(d_Camera.LookAtPoint.x, transform.position.y, d_Camera.LookAtPoint.z));
        Quaternion Rotation = Quaternion.LookRotation(new Vector3(d_Camera.LookAtPoint.x, transform.position.y, d_Camera.LookAtPoint.z) - this.transform.position);
        this.transform.rotation = Quaternion.Lerp(transform.localRotation, Rotation, Time.deltaTime * d_Camera.Damping);
        V = Input.GetAxis("Vertical");
        H = Input.GetAxis("Horizontal");
    }

    void FixedUpdate()
    {
        Move();
    }

    void Move()
    {
        if (Controller.isGrounded)
        {
            MoveDirection = DefaultDirection;
            animator.SetFloat("V", V);
            animator.SetFloat("H", H);
           
            
            JudgeSpeed();
            MoveDirection = new Vector3(H, 0, V);
            MoveDirection = transform.TransformDirection(MoveDirection);
            MoveDirection *= CurrentSpeed;
            MoveDirection = new Vector3(MoveDirection.x, 0, MoveDirection.z);
            Controller.Move(MoveDirection * Time.deltaTime);
        }
        else
        {
            y = MoveDirection.y - Gravity * Time.deltaTime;
        }

    }

    void JudgeSpeed()
    {
        AnimatorStateInfo AnimatorState = animator.GetCurrentAnimatorStateInfo(0);
        if (AnimatorState.IsName("Walk"))
        {
            CurrentSpeed = WalkSpeed;
        }
        else if (AnimatorState.IsName("Run"))
        {
            CurrentSpeed = WalkSpeed;
        }
    }
}

人物操作代码中人物移动部分与上面的行走代码是类似的,毕竟万变不离其宗。关键是这几行代码:

this.transform.LookAt(new Vector3(d_Camera.LookAtPoint.x, transform.position.y, d_Camera.LookAtPoint.z));
        Quaternion Rotation = Quaternion.LookRotation(new Vector3(d_Camera.LookAtPoint.x, transform.position.y, d_Camera.LookAtPoint.z) - this.transform.position);
        this.transform.rotation = Quaternion.Lerp(transform.localRotation, Rotation, Time.deltaTime * d_Camera.Damping);

首先使人物时刻面向我们在camera中获取的那个点的,使用LookAt()方法。然后我们使用Quaternion.Lerp()来使人物实现转动过程中平滑转身。经常使用Quaternion的人应该对这几个方法不会陌生,也可以通过查看API了解相关方法。此处不再赘述。
原本想好的要写的6种ARPG游戏人物的操作方式已经差不多了,但是sunset在编写过程中又发现了另外一种ARPG游戏的操作方式。那就是近期刚刚推出的《暗黑3》游戏的操作方式。算起来,《暗黑》系列才是ARPG游戏的鼻祖,我竟然把这种操作方式给忘记了,真是糟糕。所幸想起来了,就一并给出分析以及简单的实现代码吧。
首先,《暗黑3》的人物行走方式是主要靠鼠标完成,由鼠标点击地面,判断是否为地面,是否可以行走到该位置等几个问题来进行移动。在这里,我们不再使用上文中都快用烂了的CharacterController组件了(毕竟不止这一种行走方式),我们使用Unity3D自带的一种导航寻路组件NavMeshAgent。具体使用方法请自行参见API文档。首先我们给人物添加NavmeshAgent组件,然后bake场景中Terrain的Navgation,操作虽然有点麻烦,但也仅限于麻烦,并不难。
然后我们给人物添加操作代码,代码如下:

 //Public Variable
    public enum State
    {
        Idle,
        Move,
    }
    public float MoveSpeed;
    public float RotationSpeed;


    //Private Variable
    private NavMeshAgent navMeshAgent;
    private Animator animator;

    private float CurrentSpeed;
    private Vector3 ClickMovePosition;
    private Vector3 TargetPosition;
    private bool CanMove;
    private State state;
    private float DistanceFromTargetPosition;

    
    


    void Awake()
    {
        //获取NavMeshAgent组件
        navMeshAgent = this.GetComponent<NavMeshAgent>();
        animator = this.GetComponent<Animator>();
    }

    void Start()
    {
        navMeshAgent.angularSpeed = RotationSpeed;
        navMeshAgent.obstacleAvoidanceType = ObstacleAvoidanceType.HighQualityObstacleAvoidance;
    }
	
	// Update is called once per frame
	void Update () 
    {
        if (Input.GetMouseButtonDown(1) || Input.GetMouseButton(1))
        {
            //当点击鼠标右键,由向点击的位置发射一条射线,判断射线的碰撞体是不是地面,如果是,则执行Move();
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            //RaycastHit[] hit2Mouse;
            if (Physics.Raycast(ray, out hit))
            {
                
                ClickMovePosition = hit.transform.position;
                Debug.Log(hit.transform.position);
               // hit2Mouse = Physics.RaycastAll(this.transform.position, ClickMovePosition);
                if (hit.collider.gameObject.tag == "Terrain")
                {
                    CanMove = true;
                    TargetPosition = ClickMovePosition;
                }
               /* for (int i = 0; i < hit2Mouse.Length; i++)
                {
                    if (hit2Mouse[i].collider.gameObject.CompareTag("Terrain"))
                    {
                        CanMove = true;
                        TargetPosition = ClickMovePosition;
                    }
                }*/
                if (!CanMove)
                {
                    Debug.Log("无法移动到该地点");
                }
            }
        }
	}

    void FixedUpdate()
    {
        Move();
    }

    void Move()
    {
        if (CanMove)
        {
            state = State.Move;
            //this.transform.LookAt(TargetPosition);
            //平滑转身
            Quaternion Rotation = Quaternion.LookRotation(TargetPosition - this.transform.position);
            this.transform.rotation = Quaternion.Slerp(this.transform.rotation, Rotation, navMeshAgent.angularSpeed * Time.deltaTime);
            navMeshAgent.SetDestination(TargetPosition);
            //DistanceFromTargetPosition = Vector3.Distance(TargetPosition, this.transform.position);
            if (navMeshAgent.remainingDistance < 1.0f)//当与目标位置之间的距离小于1时,视为已到达目的地。
            {
                CanMove = false;
                state = State.Idle;
            }
        }
        if ((navMeshAgent.pathStatus == NavMeshPathStatus.PathInvalid))
        {
            state = State.Idle;
        }
        
        
    }

    void JudgeState()
    {
        if (state == State.Idle)
        {
            animator.SetBool("Move", false);
            navMeshAgent.speed = 0.0f;
        }
        else if (state == State.Move)
        {
            animator.SetBool("Move", true);
            navMeshAgent.speed = MoveSpeed;
        }
    }
}

Unity自带的NavmeshAgent还是十分好用的,带有十分强大的功能,仔细研究会有很大的收货。代码中主要是利用射线来完成的:Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);这个方法可以获取鼠标点击在屏幕中的位置信息,然后通过获取碰撞点坐标进行一系列判断,最终实现人物指定坐标点的行走方式,并自动寻路。
好的,这上面应该足足介绍了7种ARPG游戏的人物行走方式,不知道是不是足以满足制作一款简单的ARPG游戏的人物移动部分了。sunset这里的代码写的较为简单,并不难以理解,却很实用。希望能起到抛砖引玉的效果,多动脑思考,累的时候睡一觉可能会是你的工作事半功倍。欢迎留言,同时如果有大神能够光顾我的博客,请不吝赐教,谢谢。sunset明天还要进行软件工程导论的考试,考完试再更新制作ARPG游戏的下一个步骤。嗯,先去看书啦!

原创文章,转载请注明出处( https://www.cnblogs.com/Beyond1900/p/14484919.html

posted @ 2021-03-05 10:55  白杨Beyond  阅读(327)  评论(0编辑  收藏  举报