当前位置:   article > 正文

3D游戏建模与设计大作业:基于Unity平台编写游戏:打靶游戏(Target Games)_3d大作业

3d大作业

1.项目要求

  • 基础分(2分):有博客;
  •  1-3分钟视频(2分):视频呈现游戏主要游玩过程;
  •  地形(2分):使用地形组件,上面有草、树;
  •  天空盒(2分):使用天空盒,天空可随玩家位置 或 时间变化 或 按特定按键切换天空盒;
  •  固定靶(2分):有一个以上固定的靶标;
  •  运动靶(2分):有一个以上运动靶标,运动轨迹,速度使用动画控制;
  •  射击位(2分):地图上应标记若干射击位,仅在射击位附近可以拉弓射击,每个位置有 n 次机会;
  •  驽弓动画(2分):支持蓄力半拉弓,然后 hold,择机 shoot;
  •  游走(2分):玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
  •  碰撞与计分(2分):在射击位,射中靶标的相应分数,规则自定;

2.游戏简介

  打靶游戏为第一人称射击游戏,玩家通过长按鼠标左键实现拉弓,松开坐标鼠标左键则弓箭飞出。玩家通过鼠标的移动实现瞄准,通过按键盘的WSAD或上下左右四个方向键实现在地图内的移动。此外,为了营造有趣的射击环境,玩家可以按C键实现正午和夕阳模式下的天空盒的切换。游戏中有5个射击位,玩家只有在射击位上才能进行拉弓射击。游戏中有2个固定靶和3个移动靶,靶子分为红色靶心和白色靶白,击中不同颜色区域有不同的得分,移动靶在树木左右移动,难度较固定靶更大。

3.游戏规则

1)击中移动靶靶心得3分,靶白得2分。

2)击中固定靶靶心得2分,靶白得1分。

3)游戏中有5个射击位,只有在射击位上可以进行射击,每个射击位有5次射击机会。

4)当地图上所有射击位的所有射击次数被用光时,即用完25次射击次数,游戏结束。

4.游戏编写

  本游戏基于Unity引擎编写,使用的系统为Windows10,使用的脚本文件语言为C#,首先需要确保保证电脑中安装Unity引擎,我的版本是2023.3.8f1c1,还需下载VSCode或VStudio用于编写代码,还需要Unity的Plastic SCM用于管理代码,具体安装方法这里不过多介绍。

接着我们打开Unity,找到项目,选择新项目。

选择3D,项目命名为Target Games,保存地址我选择的是D盘。(启动版本管理可选可不选)。

本项目中运用到的代码、文件非常多,为了保证界面的简洁性,建议在Assests项目栏中如下图建

立几个文件夹方便管理。

 其中只有Prefabs、Scenes、Scripts、Materials和Terrain文件夹是我们要自己处理的,其它的只要需要从Assets Store中导入。

建立方法如下:在Assets栏中空白区用鼠标右键点击,选择Create中的Folder生成我们需要的游戏文件。

然后去Assets Store中导入资源,在Window中选择Asset Store。

然后点击Search online,程序会自动跳转到浏览器。

首先导入天空盒,选择添加至我的资源。

 然后同理导入弓弩和树。

 然后在Window中选择Package Manager,Unity会弹出Package Manager窗口。

 Package Manager窗口中选择My Assets。我们以导入Classical Crobow为例,选择Import。

全选后选择Import。

这样就成功导入资源,对于树木以及天空盒也同理。 

接着我们可以布置地形,布置地形、树木和草地的具体方式可以在这些链接中的教程进行。

【Unity3D】地形Terrain - 知乎 (zhihu.com)

如何在地形上绘制草丛和树木 - 技术问答 - Unity官方开发者社区

在Unity中 改变地形(Terrain),并加上水面、树、草地、材质(地板上色)_unity创建地形山草树水房子-CSDN博客

 我的地形图如下所示,其中白色长条为射击位。

为了实现玩家不会碰到靶子这一要求,可以在靶子四个方向周围设置透明的长方体Cube,但记住Cube的高度不要太高,以免射出的弓箭碰到Cube。

现在我们观察下靶子,游戏中靶子如图所示,有三片区域,红色靶心、白色区域和黑色区域,击中不同区域有不同的得分。

对于移动靶,我们需要自己制作动画,我们制作了一个名称为text的动画,具体制作过程可以·参考这个教程:Unity动画系统详解1:在Unity中如何制作动画? - 知乎 (zhihu.com) 

注意该动画只有能移动一次,至于如何来回重复移动我们需要使用脚本TargetMove来实现。

此外我们还需要制作了名称为Text的Animator。

 制作完动画后需要放入Animator,连线方式为右键动画,选择Make Transition然后选择你需要链接到的状态。

另外我们还设置了射击位,玩家在射击位中才能进行射击,如图所示:

同样为了防止玩家走出地图,我们用四个透明的大Cube来实现空气墙效果。

对于拉弓,老师课程上演示说使用Blend Tree实现,但课后我觉得十分难使用,于是制作了另外一个动画Energy Storage,这个动画事实上只是为了保持拉弓的状态,并不用设计动画的画面帧。

然后将动画按照如图所示放入Animator中,该Animator有3个trigger,一个为pull,一个为shooting,一个是hold,还有两个float,一个是Blend,一个holdTime,具体如下图所示。

其中,new hold状态为一个blend tree,由自带的动画Empty和Hold混合而成。

此外,我们还需要设置状态机的转换条件,其中Empty状态跳转到new hold状态的条件为Holding==false。

 new hold状态跳转到shoot状态的条件为shooting==false。

Shoot状态跳转到Empty状态无需额外的条件。

 我们还使用了UGUI来显示游戏得分、击中位置、游戏结束。游戏标题等信息,还在canvas中添加了Restart按钮重启游戏。

对于天空盒的布置,直接拉入Scene中即可。

我们刚刚编写的游戏界面场景Main,此外我们还要创建开始界面场景Start。

创建过程如图所示:

 在Start中创建画布canvas,如图所示:

 画布的canvas布置可以按照参考这个教程:【精选】【Unity3D-UGUI系列】(一)Canvas 画布组件详解_unity为什么显示画布内容了-CSDN博客

然后我的Start场景中布置如下:

 我们的场景布置完成,现在开始编写脚本代码,在Scripts文件夹中有这些文件夹。

我们先来实现视角移动,首先将弓弩拉入场景并命名为player,然后将主相机Main Camera移动到Player作为Player的子类。

 然后我们用脚本CameraMove来实现用鼠标进行视角360度移动,具体代码如下:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. //鼠标控制视角
  5. public class CameraMove : MonoBehaviour
  6. {
  7. //鼠标x轴灵敏度
  8. public float mouseXSensitivity = 80f;
  9. //人物
  10. private Transform player;
  11. //旋转角度
  12. float xRotation = 0f;
  13. private void Start()
  14. {
  15. player = transform.parent.transform;
  16. }
  17. // Update is called once per frame
  18. void Update()
  19. {
  20. float mouseX = Input.GetAxis("Mouse X") * mouseXSensitivity * Time.deltaTime;
  21. float mouseY = Input.GetAxis("Mouse Y") * mouseXSensitivity * Time.deltaTime;
  22. xRotation -= mouseY;
  23. //y轴最大旋转角度为正负90;
  24. xRotation = Mathf.Clamp(xRotation, -45f, 10f);
  25. transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);
  26. player.Rotate(Vector3.up * mouseX);
  27. }
  28. }

记得把CameraMove脚本挂到Main Camera上。

然后我们还需要实现玩家的移动,玩家可以通过WSAD或者方向键进行地图内的四个方向运动,我们用脚本PlayerMove实现,具体代码如下:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class PlayerMove : MonoBehaviour
  5. {
  6. //人物控制器
  7. private CharacterController controller;
  8. //人物移动速度
  9. public float speed = 2f;
  10. public float gravity = -15f;
  11. Vector3 velocity;
  12. private void Start()
  13. {
  14. controller = GetComponent<CharacterController>();
  15. }
  16. // Update is called once per frame
  17. void Update()
  18. {
  19. Move();
  20. }
  21. public void Move()
  22. {
  23. //键盘输入
  24. float x = Input.GetAxis("Horizontal");
  25. float z = Input.GetAxis("Vertical");
  26. Vector3 move = transform.right * x + transform.forward * z;
  27. controller.Move(move * speed * Time.deltaTime);
  28. velocity.y += gravity * Time.deltaTime;
  29. controller.Move(velocity * Time.deltaTime);
  30. }
  31. }

记得把PlayerMove脚本挂到Player上。

然后我们通过TargetMove脚本实现移动靶的来回移动,由于我们做的Text动画只有一个方向的单次移动,我们需要用代码实现靶子在水平方向上的多次来回移动,具体代码如下:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class TargetMove : MonoBehaviour
  5. {
  6. public float speed = 5f; // 物体移动的速度
  7. public float distance = 10f; // 物体移动的距离
  8. private Vector3 startPosition;
  9. private float direction = 1f;
  10. void Start()
  11. {
  12. startPosition = transform.position;
  13. }
  14. void Update()
  15. {
  16. // 计算物体下一帧的位置
  17. Vector3 nextPosition = transform.position + new Vector3(speed * direction * Time.deltaTime, 0f, 0f);
  18. // 判断物体是否超出移动范围,如果超出则改变移动方向
  19. if (Vector3.Distance(startPosition, nextPosition) > distance)
  20. {
  21. direction *= -1f;
  22. }
  23. // 更新物体的位置
  24. transform.position = nextPosition;
  25. }
  26. }

记得把TargetMove脚本挂到移动靶上。

现在我们来实现天空盒的切换,在该游戏中我们实现了按C键实现正午与夕阳的天空盒切换,我们是通过布尔变量isSkybox1Active来实现,默认isSkybox1Active=true,当玩家按下c键时isSkybox1Active=false,再按一次isSkybox1Active=true,我们用脚本SkyboxSwitcher来实现这一功能,具体代码如下:

  1. using UnityEngine;
  2. public class SkyboxSwitcher : MonoBehaviour
  3. {
  4. public Material skybox1; // 第一个天空盒材质
  5. public Material skybox2; // 第二个天空盒材质
  6. private bool isSkybox1Active = true; // 当前激活的天空盒
  7. private void Update()
  8. {
  9. if (Input.GetKeyDown(KeyCode.C))
  10. {
  11. SwitchSkybox();
  12. }
  13. }
  14. private void SwitchSkybox()
  15. {
  16. isSkybox1Active = !isSkybox1Active;
  17. if (isSkybox1Active)
  18. {
  19. RenderSettings.skybox = skybox1;
  20. }
  21. else
  22. {
  23. RenderSettings.skybox = skybox2;
  24. }
  25. }
  26. }

我们在本次的界面UI设计都是UGUI,在游戏界面时,当弓箭击中靶子时会显示“在XX号射击位上射中XX号靶心/靶白,加XX分”,为了能保证在击中后才显示该文本,我们需要额外编写函数设置激活态,我们用TipsText脚本实现提示隐藏,具体代码如下:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class TipsText : MonoBehaviour
  5. {
  6. public void Close()
  7. {
  8. gameObject.SetActive(false);
  9. }
  10. }

在游戏界面,我们除了要显示“在XX号射击位上射中XX号靶心/靶白,加XX分”,还需要实时显示玩家已经得了几分,我们额外用函数void SetScore(int score)来实现,脚本Tips的具体代码如下所示:

  1. using UnityEngine.UI;
  2. public class Tips : MonoBehaviour
  3. {
  4. public static Tips Instance;
  5. public GameObject tips;
  6. public Text tipsText;
  7. private int score;
  8. public Text scoreText;
  9. private void Awake()
  10. {
  11. Instance = this;
  12. }
  13. public void SetText(string str)
  14. {
  15. tipsText.text = str;
  16. tips.SetActive(true);
  17. }
  18. public void SetScore(int score)
  19. {
  20. this.score += score;
  21. scoreText.text = "当前分数:" + this.score;
  22. }
  23. }

记得要把脚本挂到控件上。 

现在我们需要实现场景切换,选择File->Build Settings。

将我们创建的场景拖入到Scenes in Build中。 

然后创建脚本Load Scene实现场景切换,具体代码如下:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.SceneManagement;
  5. public class LoadScene : MonoBehaviour
  6. {
  7. public void Load(int level)
  8. {
  9. SceneManager.LoadScene(level);
  10. }
  11. }

记得要把脚本挂到控件上。 

我们现在编写击中靶子得分的脚本Target,变量isSportTarget用于判断是否为运动靶,当isSportTarget==true时为运动靶,弓箭击中运动靶的靶心得3分,击中靶白得2分;当isSportTarget==false时为固定靶,弓箭击中固定靶的靶心得2分,击中靶白得1分;其中Circle是靶白,Bullseye是靶心。具体代码如下:

  1. using UnityEngine;
  2. public class Target : MonoBehaviour
  3. {
  4. public int score = 1; // 分数
  5. //是否为运动靶
  6. public bool isSportsTarget;
  7. private Transform point;
  8. public int indexTarget;
  9. private void Start()
  10. {
  11. point = transform.parent;
  12. }
  13. private void OnCollisionEnter(Collision collision)
  14. {
  15. if (collision.gameObject.CompareTag("Bullet"))
  16. {
  17. // 检测到子弹碰撞
  18. CalculateScore();
  19. collision.transform.GetComponent<Rigidbody>().isKinematic = true;
  20. collision.transform.position = new Vector3(collision.contacts[0].point.x, collision.contacts[0].point.y, collision.contacts[0].point.z - Random.Range(-0.3f, -0.5f));
  21. collision.gameObject.transform.parent = point;
  22. }
  23. }
  24. private void CalculateScore()
  25. {
  26. if (gameObject.tag == "Bullseye")
  27. {
  28. // 碰撞到红色靶心
  29. if (isSportsTarget)
  30. {
  31. // 在这里处理得分逻辑
  32. Tips.Instance.SetScore(3);
  33. Tips.Instance.SetText("在" + indexTarget + "号射击位上射中" + indexTarget + "号靶心,加3分");
  34. }
  35. else
  36. {
  37. Debug.Log("得到二分!");
  38. // 在这里处理得分逻辑,例如增加两分
  39. Tips.Instance.SetScore(2);
  40. Tips.Instance.SetText("在" + indexTarget + "号射击位上射中" + indexTarget + "号靶心,加2分");
  41. }
  42. }
  43. else if (gameObject.tag == "Circle")
  44. {
  45. // 碰撞到白色圆圈
  46. if (isSportsTarget)
  47. {
  48. Debug.Log("得二分!");
  49. // 在这里处理得分逻辑
  50. Tips.Instance.SetScore(2);
  51. Tips.Instance.SetText("在" + indexTarget + "号射击位上射中" + indexTarget + "号靶白,加2分");
  52. }
  53. else
  54. {
  55. Debug.Log("得到一分!");
  56. // 在这里处理得分逻辑,例如增加两分
  57. Tips.Instance.SetScore(1);
  58. Tips.Instance.SetText("在" + indexTarget + "号射击位上射中" + indexTarget + "号靶白,加1分");
  59. }
  60. }
  61. }
  62. }

记得要把Targer脚本挂到每个靶子上,且记得靶白的Tag选择为Circle。

靶心的Tag选择为Bullseye。

 此外,记得移动靶中Target脚本需要勾选变量isSportTarget,而固定靶不要勾选isSportTarget。

除了考虑弓箭与靶子的碰撞,我们还需要考虑弓箭与树木和地形的碰撞。

对于弓箭与树木的碰撞,我们使用脚本Tree实现碰撞判断,具体代码如下:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class Tree : MonoBehaviour
  5. {
  6. private void OnCollisionEnter(Collision collision)
  7. {
  8. if (collision.gameObject.CompareTag("Bullet"))
  9. {
  10. // 检测到子弹碰撞
  11. collision.transform.GetComponent<Rigidbody>().isKinematic = true;
  12. collision.transform.position = new Vector3(collision.contacts[0].point.x, collision.contacts[0].point.y, collision.contacts[0].point.z - Random.Range(-0.1f, -0.2f));
  13. }
  14. }
  15. }

记得要把Tree脚本挂在树上。 

对于弓箭与地形的碰撞,我们使用脚本Terrain实现碰撞判断,具体代码如下:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class Terrain : MonoBehaviour
  5. {
  6. private void OnCollisionEnter(Collision collision)
  7. {
  8. if (collision.gameObject.CompareTag("Bullet"))
  9. {
  10. // 检测到子弹碰撞
  11. collision.transform.GetComponent<Rigidbody>().isKinematic = true;
  12. collision.transform.position = new Vector3(collision.contacts[0].point.x, collision.contacts[0].point.y, collision.contacts[0].point.z - Random.Range(-0.5f, -0.8f));
  13. }
  14. }
  15. }

记得地形组件上需要挂载Tree和Terrain脚本。

现在我们编写判断玩家是否在射击位上的脚本ShootingArea,我们用isPlayer和isArrow判断玩家是否在射击位上,OnTriggerStay(Collider other)函数实现玩家在射击位上能做的操作,OnTriggerExit(Collider other)函数规定玩家在不在射击位上能不能做的操作,具体代码如下:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class ShootingArea : MonoBehaviour
  5. {
  6. //弓箭次数
  7. public int arrowCount = 5;
  8. //是否可以射箭
  9. public bool isArrow;
  10. private bool isPlayer;
  11. private void OnTriggerStay(Collider other)
  12. {
  13. if (isPlayer) return;
  14. if (other.gameObject.tag == "Player")
  15. {
  16. isPlayer = true;
  17. isArrow = true;
  18. other.gameObject.transform.GetComponent<Bow>().shootingArea = this;
  19. }
  20. }
  21. private void OnTriggerExit(Collider other)
  22. {
  23. if (other.gameObject.tag == "Player")
  24. {
  25. isPlayer = false;
  26. if (other.gameObject.transform.GetComponent<Bow>().shootingArea != null)
  27. {
  28. arrowCount = other.gameObject.transform.GetComponent<Bow>().shootingArea.arrowCount;
  29. }
  30. isArrow = false;
  31. other.gameObject.transform.GetComponent<Bow>().shootingArea = null;
  32. }
  33. }
  34. }

记得要把脚本ShootingArea挂到射击位控件上。

 现在我们编写控制弓箭的脚本Bow,该脚本有以下几个函数,下面简单介绍每个函数。

UpdateBowStretch():根据拉弓的距离设置弓的拉伸效果。

ShootArrow():计算蓄力时间和箭矢初速度,并实例化箭矢。

FindBullet():销毁上一次射出的箭,以免出现新箭射到射出的老箭上面。

FindShootingArea():查找判断还有发射的靶场。

LockCursor(bool a):隐藏鼠标锁鼠标

  1. using UnityEngine;
  2. using UnityEngine.UI;
  3. public class Bow : MonoBehaviour
  4. {
  5. public GameObject arrowPrefab; // 箭的预制体
  6. public Transform arrowSpawnPoint; // 箭的生成点
  7. public float maxPullDistance = 3f; // 最大拉弓距离
  8. public float maxPullForce = 100f; // 最大拉弓力量
  9. public float minPullTime = 1f; // 最小蓄力时间
  10. public float maxPullTime = 5f; // 最大蓄力时间
  11. public float arrowFlightSpeed = 10f; // 箭的飞行速度
  12. private float pullStartTime; // 开始蓄力的时间
  13. private float pullDistance; // 箭飞行距离
  14. //播放动画
  15. private Animator anim;
  16. public ShootingArea shootingArea;
  17. public Text arrowCountTxt;
  18. public GameObject arrowCount;
  19. public GameObject over;
  20. void Start()
  21. {
  22. Time.timeScale = 1;
  23. anim = GetComponent<Animator>();
  24. LockCursor(true);
  25. }
  26. private void Update()
  27. {
  28. if (Input.GetKeyDown(KeyCode.Escape) && Cursor.visible) { LockCursor(false); }
  29. if (Input.GetMouseButtonDown(0) && Cursor.visible == false) { LockCursor(true); }
  30. if (shootingArea == null)
  31. {
  32. arrowCount.SetActive(false);
  33. return;
  34. }
  35. else
  36. {
  37. arrowCount.SetActive(true);
  38. arrowCountTxt.text = "剩余箭:" + shootingArea.arrowCount;
  39. }
  40. if (shootingArea.isArrow && shootingArea.arrowCount > 0)
  41. {
  42. if (Input.GetMouseButtonDown(0))
  43. {
  44. pullStartTime = 0;
  45. anim.SetTrigger("hold");
  46. //清除所有弓箭
  47. FindBullet();
  48. }
  49. else if (Input.GetMouseButton(0))
  50. {
  51. //计算蓄力时间
  52. pullStartTime += Time.deltaTime;
  53. //设置蓄力动画
  54. anim.SetFloat("holdTime", pullStartTime);
  55. }//鼠标抬起阶段
  56. else if (Input.GetMouseButtonUp(0))
  57. {
  58. pullDistance = pullStartTime;
  59. pullStartTime = 0;
  60. anim.SetTrigger("shoot");
  61. ShootArrow();
  62. Invoke("FindShootingArea", 1.5f);
  63. }
  64. }
  65. }
  66. private void ShootArrow()
  67. {
  68. // 实例化箭矢
  69. GameObject arrow = Instantiate(arrowPrefab, arrowSpawnPoint.position, arrowSpawnPoint.rotation);
  70. Rigidbody arrowRigidbody = arrow.GetComponent<Rigidbody>();
  71. arrowRigidbody.velocity = transform.forward * pullDistance * 30f;
  72. shootingArea.arrowCount -= 1;
  73. arrowCountTxt.text = "剩余箭:" + shootingArea.arrowCount;
  74. }
  75. public void FindBullet()
  76. {
  77. var bullets = GameObject.FindGameObjectsWithTag("Bullet");
  78. for (int i = 0; i < bullets.Length; i++)
  79. {
  80. Destroy(bullets[i]);
  81. }
  82. }
  83. public void FindShootingArea()
  84. {
  85. var ShootingAreas = GameObject.FindGameObjectsWithTag("ShootingArea");
  86. var temp = 0;
  87. for (int i = 0; i < ShootingAreas.Length; i++)
  88. {
  89. if (ShootingAreas[i].transform.GetComponent<ShootingArea>().arrowCount > 0)
  90. {
  91. temp++;
  92. }
  93. }
  94. if (temp <= 0)
  95. {
  96. LockCursor(false);
  97. over.SetActive(true);
  98. Time.timeScale = 0;
  99. }
  100. }
  101. public void LockCursor(bool a)
  102. {
  103. if (a)
  104. {
  105. Cursor.lockState = CursorLockMode.Locked;
  106. Cursor.visible = false;
  107. }
  108. else
  109. {
  110. Cursor.lockState = CursorLockMode.None;
  111. Cursor.visible = true;
  112. }
  113. }
  114. }

到此,我们的游戏就编写完成了。

5.游戏演示

3D游戏编程与设计大作业:打靶游戏(Target Games)_哔哩哔哩bilibili

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/104115
推荐阅读
相关标签
  

闽ICP备14008679号