游戏截图展示:
第一人称视角设计:
建立一个空的GameObject,将之命名为Player。再把一个摄像机挂载在Player下面,再把预设好的CrossBow挂载到Camera下。
在Camera下挂载CameraControl类,在Player下挂载PlayMove类。
代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraControl : MonoBehaviour
{
public Transform player;//获取玩家
private float mouseX, mouseY;//获取鼠标位置
public float mouseSensitivity;//鼠标灵敏度
private float xRotation;
private void Update()
{
mouseX = Input.GetAxis("Mouse X") * mouseSensitivity * Time.deltaTime;
mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity * Time.deltaTime;
xRotation -= mouseY;//在上下移动视角后,防止Y的值反弹回0(若不加这一条,每次上下移动视角移动一下后,视角都会弹回原位置,不会固定)
xRotation = Mathf.Clamp(xRotation, -70f, 70f);//摄像机上下视角限制,对于Player(主角)来说,视角上下是有限制的,不然会很诡异
player.Rotate(Vector3.up * mouseX);//玩家随鼠标旋转
transform.localRotation = Quaternion.Euler(xRotation, 0, 0);//摄像机旋转
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSDirector : System.Object
{
static SSDirector _instance;
public IUserAction CurrentUserController { get; set; }
public static SSDirector GetInstance()
{
if (_instance == null)
{
_instance = new SSDirector();
}
return _instance;
}
}
public class PlayerMove : MonoBehaviour, IUserAction
{
// Start is called before the first frame update
private float walk;
private float run;
private float speed;
private Vector3 dir;
private CharacterController playerController;
private ScoreControl playersc;
public GameObject fp;
private float power;
private Animator ani;
private AnimatorStateInfo aniState;
public GameObject Cb;
void Start()
{
SSDirector.GetInstance().CurrentUserController = this;
this.gameObject.AddComponent<GameGUI>();
walk = 25.0f;
run = 50.0f;
playerController = GetComponent<CharacterController>();
playersc = new ScoreControl();
power = 0.0f;
ani = Cb.GetComponent<Animator>();
}
void Update()
{
float vertical = Input.GetAxis("Vertical");
float horizontal = Input.GetAxis("Horizontal");
if (Input.GetKey(KeyCode.Space))
speed = run;
else
speed = walk;
dir = transform.forward * vertical + transform.right * horizontal;
playerController.Move(dir * speed * Time.deltaTime);
if (Input.GetMouseButtonDown(0))
{
if (playersc.NoP || playersc.HaP)
{
if(playersc.Bullet > 0)
{
Shoot();
playersc.Bullet--;
ani.SetTrigger("shoot");
power = 0.0f;
ani.SetFloat("hold_power", 0.0f);
ani.SetFloat("power", 0.0f);
}
}
}
if (Input.GetMouseButtonDown(2))
{
aniState = ani.GetCurrentAnimatorStateInfo(0);
if (aniState.IsName("hold_n"))
{
ani.Play("Empty");
}
StartCoroutine("CheckForLongPress");
}
if (Input.GetMouseButtonUp(2))
{
StopCoroutine("CheckForLongPress");
ani.SetFloat("hold_power", power);
ani.SetTrigger("hold");
}
}
IEnumerator CheckForLongPress()
{
ani.SetTrigger("start");
power = 0;
float longPressDuration = 2.0f;
float timer = 0.0f;
while(timer < longPressDuration)
{
timer += Time.deltaTime;
power += 0.01f;
if(power >= 1)
{
power = 1;
}
ani.SetFloat("power", power);
yield return null;
}
}
public void Check()
{
float dn = (transform.localPosition.x - 5) * (transform.localPosition.x - 5) + (transform.localPosition.z + 10) * (transform.localPosition.z + 10);
float dh = (transform.localPosition.x + 10) * (transform.localPosition.x + 10) + (transform.localPosition.z + 10) * (transform.localPosition.z + 10);
if (dn <= 25)
{
if(!playersc.NoP)
{
playersc.NoP = true;
playersc.CBEnter();
}
}
else if (dh <= 25)
{
if(!playersc.HaP)
{
playersc.HaP = true;
playersc.CBEnter();
}
}
else
{
playersc.CBLeave();
}
GetComponent<GameGUI>().GM = playersc.GM;
//GetComponent<GameGUI>().Score = playersc.Score;
GetComponent<GameGUI>().NoP = playersc.NoP;
GetComponent<GameGUI>().HaP = playersc.HaP;
GetComponent<GameGUI>().Bullet = playersc.Bullet;
}
public void Shoot()
{
GameObject arrow = Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Arrow"));
arrow.gameObject.tag = "arrow";
arrow.transform.position = fp.transform.position;
arrow.transform.rotation = fp.transform.rotation;
Rigidbody rd = arrow.GetComponent<Rigidbody>();
if (rd!=null)
{
rd.AddForce(fp.transform.forward * 100 * power);
}
}
}
这样就实现了第一人称的视角移动,实现了十字弩在地图上的游走。
地形与天空盒设计:
在Scene中创建Terrain(地形)组件,将预设好的资源(树、草、地皮)布置在地形组件上。
特别地,为了使树与十字弩发生碰撞。需要将预置的树模型加上碰撞器组件。
在Camera下面创建一个空的Game Object,将之命名为SkyBox,在SkyBox中添加Skybox组件,并挂载ChangeSkyBox类的脚本,实现每10秒种切换天空背景。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ChangeSkyBox : MonoBehaviour
{
public Material[] skyboxMaterials;
void Start()
{
StartCoroutine(ChangeSkybox());
}
IEnumerator ChangeSkybox()
{
while (true)
{
// 随机选择一个天空盒子材质
int index = Random.Range(0, skyboxMaterials.Length);
RenderSettings.skybox = skyboxMaterials[index];
// 等待一段时间后再切换天空盒子
yield return new WaitForSeconds(10);
}
}
}
接着,在Unity界面右下角挂载下载好的天空盒资源。
设计移动靶子动画:
双击要创建移动动画的游戏对象,在Animation界面开始设计移动动画,首先点击界面左侧,增加游戏对象的position属性,接着点击左上角红点进行录制,以一定时间间隔设置三维坐标位置,形成动画。为保证动画的流畅性,应保持一轮动画开始的三维坐标与一轮动画结束的三维坐标一致。
设计弓弩动画:
为实现蓄力拉弓的功能,将原先的fill和hold换成混合树。
在滚轮按下时蓄力并开始记时,在滚轮松开时,停止记时,并将hold的power记录下来。
if (Input.GetMouseButtonDown(2))
{
aniState = ani.GetCurrentAnimatorStateInfo(0);
if (aniState.IsName("hold_n"))
{
ani.Play("Empty");
}
StartCoroutine("CheckForLongPress");
}
if (Input.GetMouseButtonUp(2))
{
StopCoroutine("CheckForLongPress");
ani.SetFloat("hold_power", power);
ani.SetTrigger("hold");
}
IEnumerator CheckForLongPress()
{
ani.SetTrigger("start");
power = 0;
float longPressDuration = 2.0f;
float timer = 0.0f;
while(timer < longPressDuration)
{
timer += Time.deltaTime;
power += 0.01f;
if(power >= 1)
{
power = 1;
}
ani.SetFloat("power", power);
yield return null;
}
}
GUI与计分:
将GameGUI类脚本挂载到相机下面。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameGUI : MonoBehaviour
{
IUserAction userAction;
public string GM;
public int Score;
public bool NoP;
public bool HaP;
public int Bullet;
// Start is called before the first frame update
void Start()
{
userAction = SSDirector.GetInstance().CurrentUserController as IUserAction;
}
// Update is called once per frame
void OnGUI()
{
userAction.Check();
GUI.Box(new Rect(0, 0, 150, 150), "");
GUI.Label(new Rect(10, 0, 120, 30), GM);
GUI.Label(new Rect(10, 50, 120, 30), "Score:" + Score);
GUI.Label(new Rect(10, 90, 120, 30), "子弹数量:" + Bullet);
}
}
最后通过左上角Box提示游戏信息、玩家所得分数,剩余子弹数量。
将Freeze类挂载到固定靶和移动靶上,其中移动靶被击中后会停止移动。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Freeze : MonoBehaviour
{
public GameObject Player;
void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("arrow"))
{
gameObject.GetComponent<Rigidbody>().constraints = RigidbodyConstraints.FreezeAll;
if (gameObject.GetComponent<Animator>() != null)
gameObject.GetComponent<Animator>().enabled = false;
collision.gameObject.GetComponent<Rigidbody>().constraints = RigidbodyConstraints.FreezeAll;
Player.GetComponent<GameGUI>().Score += 10;
}
}
}
计分类与接口等脚本无需挂载,将他们放到同一个脚本中即可。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface IUserAction
{
void Check();
}
public class ScoreControl
{
public string GM;
public int Score;
public bool NoP;
public bool HaP;
public int Bullet;
public ScoreControl()
{
GM = "未在可射击区域内";
Score = 0;
NoP = false;
HaP = false;
Bullet = 0;
}
public void CBEnter()
{
if (NoP)
{
Bullet = 10;
GM = "你已处于固定靶场";
}
else
{
Bullet = 20;
GM = "你已处于移动靶场";
}
}
public void CBLeave()
{
GM = "未在可射击区域内";
NoP = false;
HaP = false;
Bullet = 0;
}
}