想做一个比较完整的游戏,又不想去提前策划做什么,于是决定想到什么就做什么,看最后能做出什么来。
前段时间玩了《莱莎的炼金工坊》,觉得这游戏的移动手感挺好的,首先就做一个类似莱莎的移动系统吧。
我从AssetStore上下载了场景资源NatureStarterKit2以及人物模型资源unity-chan!,导入项目中就开始制作啦。
首先是相机跟随,一般的相机跟随很简单,只需要把相机挂载在人物上即可,但在《莱莎的炼金工坊》里,相机是可以围绕着人物转的,与人物的视角方向不一定相同,所以要另外写脚本。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraFollow : MonoBehaviour {
private GameObject Player;
private Vector3 offset;//相机和玩家初始偏移
enum RotationAxes //鼠标旋转方式
{
MouseXAndY,
MouseX,
MouseY
}
RotationAxes axes = RotationAxes.MouseXAndY;//将初始移动方式设置成xy方向都可以
public float sensitivityX = 30;//镜头敏感度
public float sensitivityY = 10;
public float minimumY = -10;//镜头上下移动限制
public float maximumY = 30;
private float rotationY = 0;
void Start()
{
FindPlayer();
initDisZ = Mathf.Abs(Player.transform.position.z - Camera.main.transform.position.z);
}
void FindPlayer()
{
Player = GameObject.FindGameObjectWithTag(Tags.Player);
offset = transform.position - Player.transform.position;
}
public void Update()
{
transform.position = Player.transform.position + offset;
if (Input.GetMouseButton(1))//右键
{
GameManager.Instance.isMoveMouse = true;
if (axes == RotationAxes.MouseXAndY)
{
float rotationX =Input.GetAxis("Mouse X");
rotationY += Input.GetAxis("Mouse Y") * sensitivityY;
rotationY = Mathf.Clamp(rotationY, minimumY, maximumY);
Camera.main.transform.localEulerAngles = new Vector3(-rotationY,Camera.main.transform.localEulerAngles.y, 0);
transform.RotateAround(Player.transform.position,Vector3.up,rotationX * sensitivityX * Time.deltaTime);//以玩家所在位置为中心,世界坐标系的向上方向为轴向(此时强调为世界坐标系,是因为玩家角色并不能保证一直处于正常的站立模式,如果是玩家自身坐标系的话,一旦玩家角色发生歪斜,相机的环绕就会出现问题),进行环绕
GameManager.Instance.forward = new Vector3(gameObject.transform.forward.x, 0, gameObject.transform.forward.z).normalized;//将行动的前方设置成相机的前方而不是人物的前方
offset = transform.position - Player.transform.position;
}
else if (axes == RotationAxes.MouseX)
{
Camera.main.transform.Rotate(0, Input.GetAxis("Mouse X") * sensitivityX, 0);
}
else
{
rotationY += Input.GetAxis("Mouse Y") * sensitivityY;
rotationY = Mathf.Clamp(rotationY, minimumY, maximumY);
Camera.main.transform.localEulerAngles = new Vector3(-rotationY, Camera.main.transform.localEulerAngles.y, 0);
}
}
if (Input.GetAxis("Mouse ScrollWheel") > 0)//滚轮控制视角范围
{
if (Camera.main.fieldOfView >= 55)
{
Camera.main.fieldOfView -= 5;
}
}
else if(Input.GetAxis("Mouse ScrollWheel") < 0)
{
if(Camera.main.fieldOfView<=85)
{
Camera.main.fieldOfView += 5;
}
}
}
}
相机的跟随部分就完成了,只需要把该脚本挂在主相机上即可。
接下来是人物行动部分。
一开始打算用2D混合树来做移动动画
后面发现《莱莎的炼金工坊》里并没有向左向右向后的概率,所有移动都是向前,只是方向不一样,之后改成了普通的动画,在代码里调整方向。然后加入了闲置时的idle动画切换,跳跃动画,用1D混合树来制作走路与跑步的切换(虽然目前没用到混合的效果)。
为了方便,我直接在OnGUI里绘制了行走和跑步之间的切换。
为了达到切换平滑的效果,我用了DoTween插件,中间也踩了一些坑,比如最后角度需要模360度,不然会出现很鬼畜的动画。
这是最终的状态机
代码
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;
public class PlayerAct : MonoBehaviour {
private Animator animator;
private float ChangeIdleTimer = 0;
private float ChangeIdleTime = 10f;
private CharacterController CharacterController;
/// <summary>
/// 动画相关
/// </summary>
// public float speedMax = 4f;
[Header("移动速度")]
public float speed = 4f;
[Header("转向速度")]
public float turnSpeed = 1f;
private bool isRunState = false;
private readonly string WaitStateName = "WAIT00";
private readonly string JumpStateName = "JUMP00B";
private readonly string WinStateName = "WIN00";
[SerializeField]
private float h;
[SerializeField]
private float v;
private Tween tween;//移动动画
void Start () {
animator = transform.GetComponent<Animator>();
CharacterController = GetComponent<CharacterController>();
}
private void Update()
{
ChangeIdleTimer += Time.deltaTime;
if(ChangeIdleTimer>ChangeIdleTime)
{
ChangeIdleState();
ChangeIdleTimer = 0;
}
}
private void ChangeIdleState()//定时切换idle状态
{
if (animator.GetCurrentAnimatorClipInfo(0)[0].clip.name.CompareTo(WaitStateName) != 0)//不在休闲状态则不切换到休闲动画
{
return;
}
int index = Random.Range(1, 5);
switch(index)
{
case 1:animator.SetTrigger("Wait1");
break;
case 2:
animator.SetTrigger("Wait2");
break;
case 3:
animator.SetTrigger("Wait3");
break;
case 4:
animator.SetTrigger("Wait4");
break;
}
}
void OnGUI()
{
Rect rect1 = new Rect(10, Screen.height - 40, 400, 30);
isRunState = GUI.Toggle(rect1, isRunState, "Run");
}
void FixedUpdate()
{
tween.Kill();//终止动画
GetAnimInput();
Move();
}
private void GetAnimInput()
{
if (Input.GetKeyDown(KeyCode.Space) && animator.GetCurrentAnimatorClipInfo(0)[0].clip.name.CompareTo(JumpStateName) != 0)//避免重复触发造成动画延迟播放
animator.SetTrigger("Jump");
if (Input.GetKeyDown(KeyCode.O) && animator.GetCurrentAnimatorClipInfo(0)[0].clip.name.CompareTo(WinStateName) != 0 && animator.GetCurrentAnimatorClipInfo(0)[0].clip.name.CompareTo(WaitStateName) == 0)
animator.SetTrigger("Win");
if (Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.W)) v = 1;
else if (Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.S)) v = -1;
else v = 0;
if (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A)) h = -1;
else if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D)) h = 1;
else h = 0;
}
private void Move()
{
Vector3 normal = Vector3.Cross(GameManager.Instance.forward, Vector3.up);
float movespeed = isRunState ? speed : speed / 2.5f;
CharacterController.SimpleMove(v * GameManager.Instance.forward * movespeed + h * (-normal) * movespeed);
int isRun = isRunState ? 1 : 0;
animator.SetFloat("V", isRun);
if (h != 0 && v != 0)
{
float turnAng = h > 0 ? 90 : 270;
float moveAng = v > 0 ? 0 : 180;
float finalAng = h < 0 && v > 0 ? 315 : (turnAng + moveAng) / 2;
tween = transform.DORotate(new Vector3(transform.eulerAngles.x, (Camera.main.transform.eulerAngles.y + finalAng) % 360), Time.fixedDeltaTime * turnSpeed);
}
else
{
if (h != 0)
{
float turnAng = h > 0 ? 90 : 270;
tween = transform.DORotate(new Vector3(transform.eulerAngles.x, (Camera.main.transform.eulerAngles.y + turnAng) % 360), Time.fixedDeltaTime * turnSpeed);
}
if (v != 0)
{
float moveAng = v > 0 ? 0 : 180;
tween = transform.DORotate(new Vector3(transform.eulerAngles.x, (Camera.main.transform.eulerAngles.y + moveAng) % 360), Time.fixedDeltaTime * turnSpeed);
}
}
if (h != 0 || v != 0)
{
("IsMoving", true);
}
else
{
animator.SetBool("IsMoving", false);
}
}
}
还有我做游戏必备的GameManager类。。虽然现在还很简单
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour {
private static GameManager _instance;
public static GameManager Instance
{
get
{
return _instance;
}
}
public Vector3 forward =new Vector3 (0, 0, 1);
private void Start()
{
if (_instance == null)
{
_instance = this;
}
}
}
然后这就是最终效果
(格式工厂转的GIF质量太差了,网页上转的质量又太好了超出大小限制。。)