前言
这是中山大学软件工程学院2023年3D游戏编程与设计的第七次作业,也欢迎大家学习参考交流
github个人主页: innitinnit (github.com)
游戏规则以及操作
这是一个第一人称的射击类小游戏。
玩家需要通过键盘以及鼠标来控制角色射击目标,在有限的射击次数中达到目标分数则获得游戏胜利。游戏中的射击目标分为动态与静态两种,并且规定只有在变换区域后可以进行射击。单次填装后,射击次数为10次。
-移动(wasd)
-蓄力(长按鼠标左键进行拉弓,松开左键进行停止蓄力;根据长按时间长短来决定发射弓箭力度)
-射击(点击鼠标右键)
-进入静态靶区域(数字键1)
-进入动态靶区域(数字2)
-返回初始位置(字母B)
-切换天空样式(数字键4)
游戏截图
游玩过程演示
1
逻辑与代码
0.0使用资源包及其下载地址
-Low-Poly Simple Nature Pack:Low-Poly Simple Nature Pack | 3D 风景 | Unity Asset Store
-Fantasy Skybox FREE:Fantasy Skybox FREE | 2D 天空 | Unity Asset Store
-十字弩:Classical Crossbow | 3D 武器 | Unity Asset Store
1.0 地形
2.0 天空盒
玩家在游玩过程中可通过按下按键4可进行天空盒的切换。
2.0.1 代码
编写一个SkyBoxController,挂载到场景中的空对象Sky Box中:
public class SkyBoxController : MonoBehaviour
{
public Material[] skyboxes; // 存储多个天空盒材质
private int currentSkyboxIndex = 0; // 当前天空盒的索引
// Update is called once per frame
void Update()
{
// 通过按键4触发天空盒切换
if (Input.GetKeyDown(KeyCode.Alpha4))
{
ChangeSkybox();
}
}
void ChangeSkybox()
{
currentSkyboxIndex = (currentSkyboxIndex + 1) % skyboxes.Length;
RenderSettings.skybox = skyboxes[currentSkyboxIndex];
}
}
设置参数,此处使用的是
3.0 射击靶
统一使用以下代码,完成箭矢与射击靶的互动:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 靶子的控制器
public class TargetController : MonoBehaviour // 用于处理靶子的碰撞
{
public int RingScore = 0; //当前靶子或环的分值
public ScoreRecorder sc_recorder;
void Start(){
sc_recorder = Singleton<ScoreRecorder>.Instance;
}
void Update(){
}
void OnCollisionEnter(Collision collision) // 检测碰撞
{
Transform arrow = collision.gameObject.transform; // 得到箭身
if(arrow == null) return;
if(arrow.tag == "arrow"){
//将箭的速度设为0
arrow.GetComponent<Rigidbody>().velocity = new Vector3(0,0,0);
//使用运动学运动控制
arrow.GetComponent<Rigidbody>().isKinematic = true;
arrow.transform.rotation = Quaternion.Euler(0, 0, 0); // 使箭的旋转角度为0
arrow.transform.parent = this.transform; // 将箭和靶子绑定
sc_recorder.RecordScore(RingScore); //计分
arrow.tag = "onTarget"; //标记箭为中靶
}
}
}
3.1 固定靶
见5.0中的代码
3.2 运动靶
同上
4.0 射击位
使用transport()函数,传送后可以进行射击。
5.0 驽弓动画
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ArrowController : MonoBehaviour, ISceneController, IUserAction
{
public CCShootManager arrow_manager; //箭的动作管理者
public ArrowFactory factory; //箭的工厂
public GameObject main_camera; // 主相机
public ScoreRecorder recorder; // 计分对象
public GameObject bow; // 弓弩
public GameObject target1, target2, target3, target4; // 四种不同的靶子和一个巨型靶子
public GameObject arrow; // 当前射出的箭
public string message = ""; // 用于显示的信息
private int arrow_num = 0; // 装载的箭的数量
public Animator ani; // 动画控制器
private List<GameObject> arrows = new List<GameObject>(); // 箭的队列
private List<GameObject> flyed = new List<GameObject>(); // 飞出去的箭的队列
public float longPressDuration = 2.0f; // 设置长按持续时间
private bool isLongPressing = false; // 是否正在长按
private float pressTime = 0f; // 长按的时间
public int state = 0; // 0-普通状态,1-拉弓状态,2-蓄力状态
public float power = 0f; // 拉弓的力度
//加载箭和靶子
public void LoadResources()
{
main_camera = transform.GetChild(5).gameObject; // 获取摄像机
bow = this.gameObject;
// 加载靶子
for(int i = 0; i < 3; ++i) // 3 * 2 = 12个靶子
{
target1 = LoadTarget("target", 10 + i, new Vector3(100+20*i, 20, 140+5*i)); // 静态小靶子
target2 = LoadTarget("big_target", 5-i, new Vector3(100+20*i, 10, 140-5*i)); // 静态大靶子
}
target3 = LoadTarget("target_move", 15, new Vector3(0,0,0)); // 慢速移动靶子, 使用动画,自带位置
target4 = LoadTarget("target_quick", 20, new Vector3(0,0,0)); // 快速移动靶子,使用动画,自带位置
}
GameObject LoadTarget(string name, int score, Vector3 pos) // 加载靶子
{
GameObject target = GameObject.Instantiate(Resources.Load("Prefabs/"+name, typeof(GameObject))) as GameObject; // 从预制中获得靶子
target.transform.position = pos;
target.AddComponent<TargetController>(); // 给靶子添加TargetController组件
target.GetComponent<TargetController>().RingScore = score;
return target;
}
//进行资源加载
void Start()
{
arrow = null;
SSDirector director = SSDirector.getInstance();
director.currentSceneController = this;
director.currentSceneController.LoadResources();
gameObject.AddComponent<ArrowFactory>();
gameObject.AddComponent<ScoreRecorder>();
gameObject.AddComponent<UserGUI>();
gameObject.AddComponent<BowController>();
factory = Singleton<ArrowFactory>.Instance;
recorder = Singleton<ScoreRecorder>.Instance;
arrow_manager = this.gameObject.AddComponent<CCShootManager>() as CCShootManager;
ani = GetComponent<Animator>();
}
void Update()
{
Shoot(); // 射击
HitGround(); // 检测箭是否射中地面
for(int i = 0; i < flyed.Count; ++i) // 用于维护已经射出去的箭的队列
{
GameObject t_arrow = flyed[i];
if(flyed.Count > 10 || t_arrow.transform.position.y < -10) // 箭的数量大于10或者箭的位置低于-10
{ // 删除箭
flyed.RemoveAt(i);
factory.RecycleArrow(t_arrow);
}
}
if(Input.GetKeyDown(KeyCode.R)) // 按R换弹
{ //从工厂得到箭
LoadArrow();
message = "弩箭已填充完毕";
}
}
public void Shoot() // 射击
{
if(arrows.Count > 0){ // 确保有箭
if(!gameObject.GetComponent<BowController>().canshoot && (Input.GetMouseButtonDown(0) || Input.GetButtonDown("Fire2"))){
message = "请按1、2到指定地点射击\n按B返回";
}
else{
aniShoot();
}
}
else{
message = "请按R键以装载弓箭";
}
}
public void aniShoot(){ // 射击的动画
if (Input.GetMouseButtonDown(0) && state==0) // 监测鼠标左键按下
{
message = "";
transform.GetChild(4).gameObject.SetActive(true); // 设置动作中的箭可见
isLongPressing = true;
ani.SetTrigger("pull");
pressTime = Time.time;
state = 1;
}
if (Input.GetMouseButtonUp(0) && state==1) // 监测鼠标左键抬起
{
isLongPressing = false;
float duration = Time.time - pressTime;
if (duration < longPressDuration){ // 执行普通点击操作
power = duration/2;
}
else{ // 拉满了
power = 1.0f;
}
ani.SetFloat("power", power);
ani.SetTrigger("hold");
state = 2;
}
if (isLongPressing && Time.time - pressTime > longPressDuration) // 长按但是未抬起,且持续时间超过设定值
{
// 长按操作
isLongPressing = false;
power = 1.0f;
ani.SetFloat("power", power);
ani.SetTrigger("hold");
}
if (Input.GetButtonDown("Fire2") && state==2){ // 鼠标右键,攻击2
transform.GetChild(4).gameObject.SetActive(false); // 设置动作中的箭不可见
ani.SetTrigger("shoot");
arrow = arrows[0];
arrow.SetActive(true);
flyed.Add(arrow);
arrows.RemoveAt(0);
arrow_manager.ArrowShoot(arrow, main_camera.transform.forward,power);
ani.SetFloat("power", 1.0f); // 恢复力度
arrow_num -= 1;
state = 0;
}
}
public void LoadArrow(){ // 获得10支箭
arrow_num = 10;
while(arrows.Count!=0){ // 清空队列
factory.RecycleArrow(arrows[0]);
arrows.RemoveAt(0);
}
for(int i=0;i<10;i++){
GameObject arrow = factory.GetArrow();
arrows.Add(arrow);
}
}
public void HitGround(){ // 检测箭是否射中地面
RaycastHit hit;
if (arrow!=null && Physics.Raycast(arrow.transform.position, Vector3.down, out hit, 2f))
{
// 如果射线与地面相交
if (hit.collider.gameObject.name == "Terrain")
{
arrow.tag = "ground";
//将箭的速度设为0
arrow.GetComponent<Rigidbody>().velocity = new Vector3(0,0,0);
//使用运动学运动控制
arrow.GetComponent<Rigidbody>().isKinematic = true;
}
}
}
//返回当前分数
public int GetScore(){
return recorder.score;
}
//得到剩余的箭的数量
public int GetArrowNum(){
return arrow_num;
}
// 显示的信息
public string GetMessage(){
return message;
}
}
6.0 游走
此处使用一个胶囊状3d物体作为玩家控制角色,因为是第一人称视角所以玩家无法看到自己的角色。
这一部分中,我们需要完成:
-角色视角随鼠标移动(CameraController.cs)
-使用键盘可以操控角色移动(PlayerController.cs)
6.0.1 代码
CameraController.cs(挂载至Camera):
public class CameraController : MonoBehaviour
{
public Transform player;//获取玩家旋转的值来进行视角旋转
private float mouseX, mouseY;//获取鼠标移动的值
public float mouseSensitivity;//鼠标灵敏度
public float xRotation;
private void Update()
{
mouseX=Input.GetAxis("Mouse X")*mouseSensitivity*Time.deltaTime;
mouseY=Input.GetAxis("Mouse Y")*mouseSensitivity*Time.deltaTime;
xRotation -= mouseY;
xRotation = Mathf.Clamp(xRotation, -70, 70);//使玩家无法无限制地上下旋转视角
player.Rotate(Vector3.up * mouseX);
transform.localRotation=Quaternion.Euler(xRotation, 0,0);
}
}
PlayerController.cs(挂载至Player):
public class PlayerController : MonoBehaviour
{
private CharacterController cc;
public float moveSpeed;
public float jumpSpeed;
private float horizontalMove, verticalMove;
private Vector3 dir;
public float gravity;
private Vector3 velocity;
//用于检测玩家是否在地面上
public Transform groundCheck;
public float checkRadius;
public LayerMask groundLayer;
public bool isGround;
private void Start()
{
cc=GetComponent<CharacterController>();
}
private void Update()
{
isGround=Physics.CheckSphere(groundCheck.position,checkRadius,groundLayer);//玩家是否碰撞地面
if (isGround && velocity.y < 0)
{
velocity.y = -2f;
}
horizontalMove = Input.GetAxis("Horizontal")*moveSpeed;
verticalMove = Input.GetAxis("Vertical") * moveSpeed;
dir=transform.forward*verticalMove+transform.right*horizontalMove;
cc.Move(dir*Time.deltaTime);
//跳跃
if(Input.GetButtonDown("Jump")&& isGround)
{
velocity.y = jumpSpeed;
}
velocity.y-=gravity*Time.deltaTime;
cc.Move(velocity*Time.deltaTime);
}
}
设置参数:
Player:
使用Character Controller组件,使角色具有碰撞体属性。
7.0 碰撞与计分
7.1碰撞
设置collider即可完成。
7.2计分
见5.0部分代码(record相关部分)