设计规则
- 地形:使用地形组件,上面有草、树;
- 天空盒:使用天空盒,天空可随玩家是否到达射击点来切换天空盒;
- 固定靶:有一个以上固定的靶标;
- 运动靶:有一个以上运动靶标,运动轨迹,速度使用动画控制;
- 射击位:地图上共两个射击位,仅在射击位附近可以拉弓射击,每个位置有5 次机会;
- 驽弓动画:支持蓄力半拉弓,然后 hold,择机 shoot;
- 游走:玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
- 碰撞与计分:在射击位,射中靶标的相应分数,规则自定;
游戏规则
动作按键
w s a d 为方向键
鼠标右键为拉弓,左键为射击
鼠标上下左右移动控制视角变化
按住shift 移动加速
游戏指引
通过地上的箭头指示下一个射击点的位置,引导玩家前往射击点。
记分规则
固定靶外靶1分,靶心2分;移动靶外靶3分,靶心4分。
部分资源和代码参考
天空盒资源使用Unity资源商店的Fantasy Skybox FREE,树资源使用Unity资源商店的SimpleNaturePack。
角色使用Unity资源商店的RyuGiKen的免费十字弩资源。
角色控制代码设计参考视频:
【Unity3D】 教你如何做一个第一人称视角的角色移动_哔哩哔哩_bilibili
游戏指引中的设计参考博客:
【Unity3D日常开发】Unity3D中实现箭头指向目标点的效果_unity引导路径箭头shader_恬静的小魔龙的博客-CSDN博客
靶子设计
外靶和靶心都用Mesh Collider实现碰撞体。
预制体scene
动画设计
蓄力射击动画
如下,除了Shoot后自动返回Empty,其他都用触发器控制。给十字弓使用。
half为Brend Tree,包括Empty和Fill,blend为0.5,如下:
移动动画
缓慢的来回移动。第一射击点,MoveTarget使用。
旋转动画
旋转往复,停止旋转和旋转间用触发器触发跳转。第二射击点,五个RotateTarget使用。
代码实现
角色部分
角色控制
角色提前添加CharacterController组件,该脚本挂载到角色上,手动设置移动速度等属性进行调试。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Xml.Schema;
using UnityEngine;
public class playerController : MonoBehaviour
{
// Start is called before the first frame update
private CharacterController cc;
public float movespeed;
public float jumpspeed;
private float h_move, v_move;
private Vector3 dir;
public float gravity;
private Vector3 velocity;
public Transform groundCheck;
public float checkRadius;
public LayerMask groundLayer;
public LayerMask shootLayer;
private bool isGround;
private Animator ani;
GameObject arrow;
// 导入箭预制件
public GameObject arrowPrefab;
// 箭发射点
public Transform arrowBegin;
// 射箭用力大小
private float force;
// 判断动画切换
string curStateName,lastStateName;
// 射击允许
private bool shootAdmitted;
// 天空
public Material sky1;
public Material sky2;
FirstSceneController controller;
GameObject playerCamera;
void Start()
{
cc = GetComponent<CharacterController>();
ani = GetComponent<Animator>();
arrow = transform.Find("箭").gameObject;
playerCamera = transform.Find("第一人称视角").gameObject;
lastStateName = ani.GetCurrentAnimatorClipInfo(0)[0].clip.name;
shootAdmitted =false;
controller = SSDirector.GetInstance().currentSceneController as FirstSceneController;
}
// Update is called once per frame
void Update()
{
// 实现角色相机和主游戏相机切换
if (controller.GetState() == (int)GameState.Ready || controller.GetState() == (int)GameState.GameOver)
{
playerCamera.SetActive(false);
return;
}
else
{
playerCamera.SetActive(true);
}
// 角色落地
if (isGround && velocity.y<0)
{
velocity.y = -2f;
}
// 到达射击点
shootAdmitted = Physics.CheckSphere(groundCheck.position, checkRadius, shootLayer);
// 申请射击许可
if(shootAdmitted)controller.setGameState((int)GameState.Shooting);
else controller.setGameState((int)GameState.Play);
// 到达或离开射击点后,天空盒转换
if (controller.checkShoot())
{
transform.Find("第一人称视角").gameObject.GetComponent<Skybox>().material = sky2;
}
else
{
transform.Find("第一人称视角").gameObject.GetComponent<Skybox>().material = sky1;
}
// 如果按住shift键,加速
if(Input.GetKey("left shift"))
{
movespeed = 15;
}
else
{
movespeed = 5;
}
// 当前动画如果是Blend Tree(half),取half第二个动画名Fill
if (ani.GetCurrentAnimatorClipInfoCount(0) > 1)
{
curStateName = ani.GetCurrentAnimatorClipInfo(0)[1].clip.name;
}
else
{
curStateName = ani.GetCurrentAnimatorClipInfo(0)[0].clip.name;
}
// 蓄力,自带的箭可见;射出,自带的箭不可见
if (curStateName == "Fill")
{
arrow.SetActive(true);
}else if (curStateName == "Shoot")
{
arrow.SetActive(false) ;
}
// 右键触发蓄力,力量加10
if (Input.GetButtonDown("Fire2")&& controller.checkShoot())
{
ani.SetTrigger("fill");
force += 10;
}
// 限制力量最大为20
force = Mathf.Clamp(force, 0f, 20f);
// 左键触发射击,射击成功将生成一支箭并射出
if (Input.GetButtonDown("Fire1") && controller.checkShoot())
{
if(lastStateName == "Fill" || lastStateName == "Hold")
{
GameObject new_arrow = Instantiate<GameObject>(arrowPrefab, arrowBegin.position, transform.localRotation);
new_arrow.GetComponent<Rigidbody>().AddForce(transform.forward * force);
controller.Shooted();
force = 0;
}
ani.SetTrigger("shoot");
}
// lastStateName更新最新动画状态
if(curStateName != lastStateName)
{
lastStateName = curStateName;
}
// 前后左右移动
h_move = Input.GetAxis("Horizontal")*movespeed;
v_move = Input.GetAxis("Vertical")*movespeed;
dir = transform.forward * v_move + transform.right * h_move;
cc.Move(dir*Time.deltaTime);
isGround = Physics.CheckSphere(groundCheck.position, checkRadius,groundLayer);
// 下落
velocity.y -= gravity*Time.deltaTime;
cc.Move(velocity*Time.deltaTime);
}
// 是否到达射击点
public bool arrived()
{
if (shootAdmitted)
return true;
return false;
}
// 所有射击点的箭都用完
public bool ArrowNull()
{
return controller.ArrowNull();
}
}
视野控制
挂载在角色的相机子对象“第一人称视角”上,提前设计好该对象的位置,手动设置鼠标灵敏度等属性。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class cameraController : MonoBehaviour
{
// Start is called before the first frame update
public Transform player;
private float mouseX, mouseY;
public float mouseSensitivity;
public float xRotation;
public float yRotation;
private void Update()
{
mouseX = Input.GetAxis("Mouse X")*mouseSensitivity*Time.deltaTime;
mouseY = Input.GetAxis("Mouse Y")*mouseSensitivity*Time.deltaTime;
xRotation += mouseX;
yRotation -= mouseY;
yRotation = Mathf.Clamp(yRotation, -50f, 50f);
player.localRotation= Quaternion.Euler(yRotation, xRotation, 0);
}
}
箭与靶子的碰撞检测
挂载在箭预制件上,检测箭是否与靶子碰撞,并控制靶子的动画变化。
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class coll : MonoBehaviour
{
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.tag.CompareTo("target_1") == 0)
{
/*插在固定靶上*/
this.gameObject.GetComponent<Rigidbody>().isKinematic = true;
singleton<ScoreRecorder>.Instance.record(1);// 1分
}
else if (collision.gameObject.tag.CompareTo("target_2") == 0)
{
/*插在固定靶心上*/
this.gameObject.GetComponent<Rigidbody>().isKinematic = true;
singleton<ScoreRecorder>.Instance.record(2);// 2分
}
else if (collision.gameObject.tag.CompareTo("target_move_1") == 0)
{
/*插在移动靶*/
this.gameObject.GetComponent<Rigidbody>().isKinematic = true;// 运动学
this.gameObject.transform.parent = collision.gameObject.transform;
//collision.gameObject.GetComponent<Rigidbody>().AddForce(this.gameObject.GetComponent<Rigidbody>().velocity);
singleton<ScoreRecorder>.Instance.record(3);// 3分
// 旋转靶 停止,再次射中就重新旋转
if (collision.transform.parent.gameObject.name[0].CompareTo('R') == 0)
{
Animator ani = collision.transform.parent.gameObject.GetComponent<Animator>();
string curStateName = ani.GetCurrentAnimatorClipInfo(0)[0].clip.name;
Debug.Log(curStateName);
if (curStateName=="Rotate")
ani.SetTrigger("end");
else
ani.SetTrigger("start");
}
}
else if (collision.gameObject.tag.CompareTo("target_move_2") == 0)
{
/*插在移动靶靶心*/
this.gameObject.GetComponent<Rigidbody>().isKinematic = true;// 运动学
this.gameObject.transform.parent = collision.gameObject.transform;
singleton<ScoreRecorder>.Instance.record(4);// 4分
// 旋转靶 停止,再次射中就重新旋转
if (collision.transform.parent.gameObject.name[0].CompareTo('R') == 0)
{
Animator ani = collision.transform.parent.gameObject.GetComponent<Animator>();// 靶心
string curStateName = ani.GetCurrentAnimatorClipInfo(0)[0].clip.name;
Debug.Log(curStateName);
if (curStateName == "Rotate")
ani.SetTrigger("end");
else
ani.SetTrigger("start");
}
}
else
{
Destroy(gameObject);
}
}
// Start is called before the first frame update
void Start()
{
Color color = (Random.Range(1, 4) == 1) ? Color.green : (Random.Range(1, 4) == 2) ? Color.red : Color.blue;
GameObject.Find("MoveTrack").GetComponent<TrailRenderer>().startColor = color;
}
// Update is called once per frame
void Update()
{
/*需要改成回收*/
if(gameObject.transform.position.y <-10)
Destroy(gameObject);
}
}
路径显示
挂载在地形对象Map上,使用箭头图形材质球,points列表首位为玩家,第二则是射击点对象。只有角色没到射击点并且没有用完所有射击点的箭时,画箭头路径,指向当前射击点。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 导航箭头
/// </summary>
public class NavPathArrow : MonoBehaviour
{
public MeshRenderer meshRenderer;//箭头3D对象Quad
public List<Transform> points = new List<Transform>();//路径点
private List<MeshRenderer> lines = new List<MeshRenderer>();//显示的路径
public float xscale = 1f;//缩放比例
public float yscale = 1f;
bool draw = true;
void Start()
{
//箭头宽度缩放值
xscale = meshRenderer.transform.localScale.x;
//箭头长度缩放值
yscale = meshRenderer.transform.localScale.y;
}
//画路径
public void DrawPath()
{
if (points == null || points.Count <= 1)
return;
for (int i = 0; i < points.Count - 1; i++)
{
DrawLine(points[i].position, points[i + 1].position, i);
}
}
//画路径 参数为路径点数组
public void DrawPath(Vector3[] points)
{
if (points == null || points.Length <= 1)
return;
for (int i = 0; i < points.Length - 1; i++)
{
DrawLine(points[i], points[i + 1], i);
}
}
//隐藏路径
public void HidePath()
{
for (int i = 0; i < lines.Count; i++)
lines[i].gameObject.SetActive(false);
}
//画路径
private void DrawLine(Vector3 start, Vector3 end, int index)
{
//Debug.Log(transform.gameObject.name);
MeshRenderer mr;
if (index >= lines.Count)
{
mr = Instantiate(meshRenderer);
lines.Add(mr);
}
else
{
mr = lines[index];
}
var tran = mr.transform;
var length = Vector3.Distance(start, end);
tran.localScale = new Vector3(xscale, length, 1);
start.y = end.y;
tran.position = (start + end) / 2;
//指向end
tran.LookAt(end);
//旋转偏移
tran.Rotate(90, 0, 0);
mr.material.mainTextureScale = new Vector2(1, length * yscale);
mr.gameObject.SetActive(true);
}
void Update()
{
if (draw)
{
HidePath();
DrawPath();
}
else
{
HidePath();
}
// 没到目的地且所有射击点的箭还没射完就画线
if (!points[0].GetComponent<playerController>().arrived() && !points[0].GetComponent<playerController>().ArrowNull())
{
draw = true;
}
else
{
draw = false;
}
}
}
场景控制
FirstController
实现场景控制,包括计分器、场景加载,并且实现用户界面接口方法,挂载在一直存在的一个相机对象上。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.UIElements;
enum GameState
{
Ready = 0, Play = 1, Shooting = 2, GameOver = 3
};
enum scene_children
{
player = 0,target = 1, shootpoint = 2, environment = 3
}
public class FirstSceneController : MonoBehaviour, ISceneController,IUserAction
{
ScoreRecorder scoreRecorder;
int arrowNum = 0;
int game_state = 0;
private GameObject shootPoint;
private GameObject scene;
private List<Vector3> shootPointPositions = new List<Vector3>();
private int shootPointNo;
private void Awake()
{
SSDirector director = SSDirector.GetInstance();
director.currentSceneController = this;
scoreRecorder = gameObject.AddComponent<ScoreRecorder>() as ScoreRecorder;
shootPointPositions.Add(new Vector3(0.1f, 0.1f, 9.1f));
shootPointPositions.Add(new Vector3(5.4f, 0.1f, -22.5f));
shootPointNo = 0;
}
public void GameStart()
{
LoadResources();
if(game_state == 0)
{
game_state = (int)GameState.Play;
shootPointNo = 0;
if (shootPointNo < shootPointPositions.Count)
shootPoint.transform.position = shootPointPositions[shootPointNo];
}
}
public void LoadResources()
{
/*加载地形*/
scene = GameObject.Instantiate(Resources.Load<GameObject>("Prefabs/scene"), Vector3.zero, Quaternion.identity);
shootPoint = scene.transform.GetChild((int)scene_children.shootpoint).gameObject;
this.gameObject.GetComponent<Camera>().targetDisplay = 1;
}
public void Restart()
{
/*开始状态*/
setGameState(0);
/*得分恢复*/
scoreRecorder.set_zero();
}
public void End()
{
setGameState(3);
this.gameObject.GetComponent<Camera>().targetDisplay = 0;
}
public int GetState()
{
/*返回当前游戏状态*/
return game_state;
}
public int GetScore()
{
/*获取计分器的分数*/
return scoreRecorder.get_score();
}
public int GetArrowNum()
{
/*获取箭数*/
if(game_state == (int)GameState.Shooting)
return arrowNum;
return 0;
}
public bool checkShoot()
{
if (game_state != (int)GameState.Shooting || arrowNum == 0)
return false;
else
return true;
}
public void setArrowNum(int num)
{
if(!checkShoot())
arrowNum = num;
}
public void Shooted()
{
arrowNum -= 1;
}
public void setGameState(int i)
{
if (i == 0 && game_state == (int)GameState.GameOver)
{
game_state = (int)GameState.Ready;
}
else if(i == 2&&game_state == (int)GameState.Play && shootPointNo < shootPointPositions.Count)
{// 如果游戏开始 且 进入新射击点
game_state = (int)GameState.Shooting;
setArrowNum(5);
}
// 开始射击
else if(i == 1&&game_state == (int)GameState.Shooting)
{
game_state = (int)GameState.Play;
}
//游戏结束
else if (i == 3)
{
game_state = (int)GameState.GameOver;
scene.SetActive(false);
}
}
// 所有射击点的箭都用完
public bool ArrowNull()
{
return shootPointNo >= shootPointPositions.Count;
}
private void Update()
{
// 射击完后
if (game_state == (int)GameState.Shooting && arrowNum == 0 )
{
game_state = (int)GameState.Play;
// 移动射击点
shootPointNo += 1;
if (shootPointNo < shootPointPositions.Count)
shootPoint.transform.position = shootPointPositions[shootPointNo];
}
//Debug.Log(game_state);
}
}
SSDirector
导演类,帮助获取场景控制器等对象单例。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SSDirector : System.Object
{
private static SSDirector instance;
public ISceneController currentSceneController { get; set; }
public static SSDirector GetInstance()
{
if (instance == null)
{
instance = new SSDirector();
}
return instance;
}
}
单例模板类
由于直接获取唯一的计分器等。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class singleton<T> : MonoBehaviour where T : MonoBehaviour
{
protected static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = (T)FindObjectOfType(typeof(T));
if (instance == null)
{
Debug.LogError("An instance of " + typeof(T)
+ " is needed in the scene, but there is none.");
}
}
return instance;
}
}
}
ScoreRecorder
计分器,负责分数管理
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreRecorder : MonoBehaviour
{
// Start is called before the first frame update
int score;
void Start()
{
score = 0;
}
public void set_zero()
{
score = 0;
}
public void record(int sc) { score += sc; }
public int get_score() { return score; }
}
UserGUI玩家界面
挂载在一直存在的相机对象上,实现各个游戏状态的用户界面显示,额外显示加分提示作为得分反馈。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour
{
private IUserAction action;
private bool game_start;
private GUIStyle style = new GUIStyle();
private GUIStyle style2 = new GUIStyle();
private GUIStyle style3 = new GUIStyle();
private GUIStyle style4 = new GUIStyle();// 规则字体
FirstSceneController controller;
public int time;// 加分提示显示
int time_count;// 加分显示计时
int pre_score;// 加分前得分
string rules;// 规则
// Start is called before the first frame update
void Start()
{
action = SSDirector.GetInstance().currentSceneController as IUserAction;
game_start = false;
style.normal.textColor = Color.red;
style.fontSize = 16;
style2.normal.textColor = new Color(1, 1, 1);
style2.fontSize = 25;
style3.normal.textColor = new Color(0, 1, 0);
style3.fontSize = 17;
style4.normal.textColor = Color.black;
style4.fontSize = 17;
pre_score = 0;
rules = "w s a d 为方向键\n鼠标右键为拉弓\n鼠标左键为射击\n按住shift 移动加速\n固定靶外靶1分,靶心2分\n移动靶外靶3分,靶心4分";
controller = SSDirector.GetInstance().currentSceneController as FirstSceneController;
}
// Update is called once per frame
void Update()
{
}
private void OnGUI()
{
// 提示计时
if (controller.GetScore() > pre_score && time_count < 0)
{
time_count = 0;
}
if(time_count >= 0)
{
time_count++;
}
if (time_count > time)
{
pre_score = controller.GetScore();
time_count = -1;
}
if (controller.GetState() == (int)GameState.GameOver)
{
game_start=false;
pre_score = 0;
GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height / 3, 200, 100), "游戏结束\n你的得分是" + action.GetScore().ToString(), style2);
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2 , 100, 50), "重新开始"))
{
controller.Restart();
}
}
else if (game_start)
{
GUI.Label(new Rect(10, 5, 200, 50), "分数:", style);
GUI.Label(new Rect(55, 5, 200, 50), pre_score.ToString(), style);
GUI.Label(new Rect(85, 5, 200, 50),((action.GetScore()-pre_score>0)? "+"+(action.GetScore() - pre_score).ToString():""), style3);
GUI.Label(new Rect(Screen.width-100, 30, 200, 50), "箭支数: ", style);
GUI.Label(new Rect(Screen.width-50, 30, 200, 50), action.GetArrowNum().ToString(),style);
GUI.Label(new Rect(10, Screen.height-150, 200, 200), rules, style4);
if (GUI.Button(new Rect(Screen.width-100, 0, 100, 30), "结束游戏"))
{
action.End();
return;
}
}
else
{
GUI.Label(new Rect(Screen.width / 2 - 80, Screen.height / 3, 100, 100), "Arrow Shooting", style2);
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2 , 100, 50), "游戏开始"))
{
game_start = true;
controller.GameStart();
}
}
}
}
接口类
场景控制接口和用户动作接口
using System;
public interface ISceneController
{
void LoadResources();
}
public interface IUserAction
{
void End();
void Restart();
int GetState();
int GetScore();
int GetArrowNum();
}
游戏演示
大作业 - 打靶游戏