当前位置:   article > 正文

Unity游戏制作之射箭游戏_unity射箭教程

unity射箭教程

目录

前言

具体实现

1. 人物游走

2. 地形

3. 天空盒变换

​编辑

4. 靶子制作&&碰撞与计分

固定靶

运动靶

5. 射击位的实现

6. 弓弩变化

7. 其他部分

总结


前言

         3D游戏制作的大作业,先放要求:

        游戏成果截图:

下面将按功能讲解具体实现

具体实现

        游戏的整体框架沿用之前的架构,用工厂单实例来产生箭射出。整体结构如下:

        工欲善其事,必先利其器。首先要导入资源包,包括天空盒资源、树草资源、靶子资源和十字弩资源。这些都是免费资源~

1. 人物游走

        游戏需要让玩家在地图中以第一人称视角在地图上游走,包括前后左右移动、跳跃和移动视角。首先要创建玩家对象,这里创建的是一个胶囊体capsule,去除掉原来的胶囊碰撞体,添加Character Controller作为新的碰撞体。

        为了实现第一人称的效果,把相机调整到人物的上半部分,并挂载到人物上作为人物的子对象。射击用的十字弩也调整到合适位置并,挂载到相机下作为相机的子对象,这样摄像机移动时弓也能同步移动。为了检测玩家到地面的距离(防止一直跳上天的bug),还要添加一个检测对象在人物底部。这样对玩家对象的创建就结束了。

        (个人想法)胶囊体什么都没有感觉有点突兀,所以直接在mesh render中去掉材料了,按一个减号就行,这样就能让人物变得透明同时还有碰撞体积。

        用代码来控制人的移动、跳跃和相机视角的转动,下面两个代码分别挂载到人物和相机上。这两段代码是在网上搜的,但是现在找不到来源了……

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class PlayerController : MonoBehaviour
  5. {
  6. //获得player的CharacterController组件
  7. private CharacterController cc;
  8. public float moveSpeed = 80.0f;//移动速度
  9. public float jumpSpeed = 50.0f;//跳跃速度
  10. //定义获得按键值的两个变量
  11. private float horizontalMove, verticalMove;
  12. //定义三维变量dir控制方向
  13. private Vector3 dir;
  14. //重力
  15. private float gravity = 9.8f;
  16. private Vector3 velocity;//用来控制Y轴速度
  17. //我们只需要检测player是否在地上就可以了,这里我们可以使用Physics中的CheckSphere方法,如果定义的球体和物体发生碰撞,返回真
  18. //为了使用这个方法,我们需要定义几个变量
  19. public Transform groundCheck;//检测点的中心位置
  20. public float checkRedius;//检测点的半径
  21. public LayerMask groundLayer;//需要检测的图层
  22. //布尔值来存储CheckSphere的返回值
  23. public bool isGround;
  24. private void Start()
  25. {
  26. //获取player的CharacterController组件
  27. cc = GetComponent<CharacterController>();
  28. //锁定鼠标后再解锁,鼠标将自动回到屏幕中心
  29. Cursor.lockState = CursorLockMode.Locked;
  30. // Cursor.lockState = CursorLockMode.None;
  31. //隐藏鼠标
  32. Cursor.visible = false;
  33. }
  34. private void Update()
  35. {
  36. isGround = Physics.CheckSphere(groundCheck.position,checkRedius,groundLayer);
  37. if(isGround && velocity.y < 0)
  38. {
  39. velocity.y = -2f;
  40. }
  41. horizontalMove = Input.GetAxis("Horizontal") *2 * moveSpeed;
  42. verticalMove = Input.GetAxis("Vertical") * 2 * moveSpeed;
  43. dir = transform.forward * verticalMove + transform.right * horizontalMove;
  44. cc.Move(dir * Time.deltaTime);
  45. //在一瞬间有一个向上的速度,在过程中也会随着重力慢慢下降,如果想要让它只跳跃一次的话,加上isGround就行了
  46. if(Input.GetKeyDown(KeyCode.Space) && isGround)
  47. {
  48. velocity.y = jumpSpeed*1.4f;
  49. }
  50. velocity.y -= gravity * Time.deltaTime;
  51. //再用CharacterController的Move方法来移动y轴
  52. cc.Move(velocity * Time.deltaTime);
  53. }
  54. }

        控制相机跟随:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class CameraController : MonoBehaviour
  5. {
  6. //我们通过控制Player的旋转方法,来控制相机视角的左右移动,所以我们需要一个Player的Tranform
  7. public Transform player;
  8. //定义两个float变量,来获取鼠标移动的值
  9. private float mouseX, mouseY;
  10. //我们可以给鼠标增加一个灵敏度
  11. public float mouseSensitivity = 800f;
  12. //mouseY中的GetAxis方法会返回-1到1之间的浮点数,在鼠标移动的时候,数值会随着方向的变化而变化,在鼠标不动时,数值会回弹到0,所以我们就会遇到鼠标上下移动时回弹的问题
  13. public float xRotation;
  14. private void Update()
  15. {
  16. //在Update方法中,我们使用输入系统中的GetAxis方法来获取鼠标移动的值,乘以鼠标灵敏度再乘以Time.deltatime,鼠标移动的值就这样得到了
  17. //Input.GetAxis:它会在鼠标移动相应对应轴的过程中返回 -1 到 1 的值
  18. mouseX = Input.GetAxis("Mouse X") * mouseSensitivity * Time.deltaTime;
  19. mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity * Time.deltaTime;
  20. xRotation -= mouseY;
  21. //使用数学函数Clamp限制
  22. xRotation = Mathf.Clamp(xRotation,-50f,60f);
  23. //Vector3.up是向上的一个三维变量,和一个0,1,0的三维变量是一样的
  24. //我们需要控制player的y轴旋转才能让它左右旋转
  25. player.Rotate(Vector3.up * mouseX);
  26. //接下来我们要选转相机了,我们使用tranform.localRotation方法,让相机上下旋转,使用localRotation就可以不被父对象旋转影响,造成一些奇怪的问题
  27. //因为localRotation是属性,我们还要给他赋值
  28. transform.localRotation = Quaternion.Euler(xRotation, 0, 0);
  29. }
  30. }

2. 地形

        自行创建地形terrain,用自带的工具自行发挥创作。为了让上面的groundcheck能检测到东西,需要给地形的Layer设置成Ground,这个是需要自己创建并添加的。

        

        种的树和草用的都是导入资源包里的预制。初始的预制是没有碰撞体积的,要想有碰撞体积,我采取的方法是编辑预制,根据树的形状手动加碰撞体,然后用让人物变透明的一样的方法让拟合的物体变透明。

3. 天空盒变换

        天空盒资源用的是导入的资源包Fantasy Skybox FREE。作业要求是天空盒变化就好,但为了视觉效果更真实,让光照也跟着天空盒一起变化,光照的方向尽可能和贴图中太阳的方向一样。为了让场景的视觉效果更好,还调整了一系列参数,参考的教程是这个:

Unity中调整光照特效的7个技巧_unity怎么让场景变亮-CSDN博客

        下面是控制天空盒变化的代码,里面的一些数字是根据场景手动调整的。

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class ChangeSkyBox : MonoBehaviour
  5. {
  6. public Material[] mats;
  7. private int index=0;
  8. // 获取Directional Light组件
  9. private Light directionalLight;
  10. public Gradient lightColorGradient;
  11. public AnimationCurve lightIntensityCurve;
  12. // Start is called before the first frame update
  13. void Start()
  14. {
  15. mats = Resources.LoadAll<Material>("Materials/skyboxs");
  16. RenderSettings.skybox = mats[0];
  17. index ++;
  18. directionalLight = GameObject.Find("Directional Light").GetComponent<Light>();
  19. }
  20. // Update is called once per frame
  21. void Update()
  22. {
  23. if(Input.GetKeyDown(KeyCode.LeftShift)){
  24. ChangeBox();
  25. }
  26. ChangeLight();
  27. }
  28. public void ChangeBox()
  29. {
  30. RenderSettings.skybox = mats[index];
  31. index++;
  32. index %= mats.Length;
  33. }
  34. public void ChangeLight(){
  35. float currentTime = index*6f;
  36. // 计算当前的光强度
  37. float intensity = lightIntensityCurve.Evaluate(currentTime);
  38. // 计算当前的光颜色
  39. Color color = lightColorGradient.Evaluate(currentTime / 24f);
  40. // 设置光源的方向(模拟太阳的移动)
  41. float rotationAngle = (currentTime - 5f) * 15f; // 每小时15度
  42. directionalLight.transform.rotation = Quaternion.Euler(rotationAngle, -85f, 0f);
  43. // 设置光源的强度和颜色
  44. // directionalLight.intensity = intensity;
  45. directionalLight.color = color;
  46. }
  47. }

        下面分别是早晨(6:00),中午(12:00),傍晚(18:00)和夜晚(24:00)的效果:

4. 靶子制作&&碰撞与计分

        靶子的资源用的是Military target(这个模型不知道为什么很小,放大50倍感觉才正常点)。要求射中不同区域得不同的分数,所以需要修改一下。这里把靶子分成3个有效击中区域,给每个区域制作一个用来检测碰撞的射击环(其实是个圆盘)。环的大小和厚度都是手动调整的,调成透明的方法也和前面一样。

        手动添加的碰撞体积用的都是mesh colider,其中除了background,都勾选上isTrigger选项,这样代码才能通过OnTriggerEnter函数检测到碰撞(勾选这个选项后实际上没有碰撞效果,箭会穿过去,但是后面有background兜着,所以不会穿过去)。

        计分就由挂载到环上的代码来实现,当发生碰撞后,就把箭停下,然后设成靶子的子对象,并且使用运动学控制,这样就能实现箭插在靶子上的效果。碰撞发生后,用专门的计分器ScoreController计分。

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class RingController : MonoBehaviour
  5. {
  6. //当前环的分值
  7. public int RingScore ;
  8. public ISceneController scene;
  9. public ScoreRecorder sc_recorder;
  10. // Start is called before the first frame update
  11. void Start()
  12. {
  13. scene = Director.getInstance().currentSceneController as FirstController;
  14. sc_recorder = Singleton<ScoreRecorder>.Instance;
  15. }
  16. // Update is called once per frame
  17. void Update()
  18. {
  19. }
  20. //碰撞检测,如果箭击中该环,就响应。
  21. void OnTriggerEnter(Collider collision){
  22. Debug.Log("trigger");
  23. Transform arrow = collision.gameObject.transform;
  24. Debug.Log(arrow);
  25. if (arrow == null)
  26. {
  27. return;
  28. }
  29. if (arrow.tag == "arrow")
  30. {
  31. //将箭的速度设为0
  32. arrow.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);
  33. // Debug.Log("击中"+RingScore);
  34. //使用运动学运动控制
  35. arrow.GetComponent<Rigidbody>().isKinematic = true;
  36. arrow.transform.parent = this.transform.parent; // 将箭和靶子绑定
  37. //计分
  38. sc_recorder.Record(RingScore);
  39. }
  40. }
  41. }
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using System.Runtime.CompilerServices;
  4. using UnityEngine;
  5. public class ScoreRecorder : MonoBehaviour
  6. {
  7. int score;
  8. public FirstController firstController;
  9. public UserGUI userGUI;
  10. // Start is called before the first frame update
  11. void Start()
  12. {
  13. firstController = (FirstController)Director.getInstance().currentSceneController;
  14. firstController.scoreController = this;
  15. userGUI = this.gameObject.GetComponent<UserGUI>();
  16. }
  17. public void Record(int ringscore) {
  18. score += ringscore;
  19. userGUI.score = score;
  20. }
  21. }

固定靶

        把靶子加载到固定位置即可。我在这里受平时玩的一个fps游戏启发,设计了一个可交互的固定靶,可以自行调整一个固定靶的位置。

        实现逻辑很简单,获取游戏对象,改变位置即可。为了方便(其实好像也不方便),这里在每个块上面都挂了代码,这里展示4m的:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class Interact : MonoBehaviour
  5. {
  6. GameObject target; //需要移动的游戏对象
  7. Vector3 origin_position;
  8. Transform origin_transform;
  9. // Start is called before the first frame update
  10. void Start()
  11. {
  12. origin_position = new Vector3(75f,8f,55f);
  13. target = GameObject.Find("move-fixed");
  14. // Debug.Log(origin_transform.position);
  15. }
  16. // Update is called once per frame
  17. void Update()
  18. {
  19. }
  20. void OnTriggerEnter(Collider collision)
  21. {
  22. // 获取被碰撞的物体
  23. GameObject collidedObject = collision.gameObject;
  24. if(collidedObject.tag == "arrow"){
  25. // Debug.Log(origin_transform.position);
  26. target.transform.position = origin_position + new Vector3(0,0,8f);
  27. }
  28. }
  29. }

运动靶

        给游戏对象自行制作相应的动画animation,上课都说过,这里不再介绍。

5. 射击位的实现

       射击位也是通过碰撞实现。把射击区域设计成偏平的立方体,刚好能检测到碰撞的那种,这样看起来就像是地毯了。挂载代码到射击区域上,玩家进入区域时才通知firstcontroller可以射击。

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class ShootArea : MonoBehaviour
  5. {
  6. //是否可以射箭
  7. private bool canShoot;
  8. public FirstController firstController;
  9. void Start(){
  10. firstController = (FirstController)Director.getInstance().currentSceneController;
  11. }
  12. public void OnTriggerEnter(Collider collider)
  13. {
  14. if (collider.gameObject.tag == "Player")
  15. {
  16. canShoot = true;
  17. firstController.AreaCallBack(canShoot);
  18. }
  19. }
  20. private void OnTriggerExit(Collider collider)
  21. {
  22. if (collider.gameObject.tag == "Player")
  23. {
  24. canShoot = false;
  25. firstController.AreaCallBack(canShoot);
  26. }
  27. }
  28. }

6. 弓弩变化

        根据导入资源的动作自行制作一个状态机,和上课讲的状态机一样,其中empty-pull和hold都是混合树,通过这两个混合树来实现蓄力拉弓的动作。

        射箭的逻辑由ShootControl控制,左键点击射箭,左键长按蓄力,长按时点击右键hold,hold后再次点击左键射出。通过一系列if else判断来检测鼠标状态,其中长按的检测用到了简单的协程;通过设置参数来控制动画,参数设置用setTrigger函数和setFloat实现。

        还要从firstController中获取是否能射箭和剩余箭数量的信息,只有在设计区域并且有箭时才可以射箭。

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class ShootControl : MonoBehaviour
  5. {
  6. public Animator animator;
  7. public FirstController firstController;
  8. public float power = 0.4f;
  9. private bool isHolding = false;
  10. private bool isMouseDown;
  11. private bool isMouseLongPressed;
  12. private float longPressDuration = 0.2f; // 定义长按的持续时间
  13. void Start(){
  14. animator = GetComponent<Animator>();
  15. firstController = (FirstController)Director.getInstance().currentSceneController;
  16. }
  17. private void Update()
  18. {
  19. // 长按左键不断增加力量
  20. if (isMouseLongPressed && !isHolding)
  21. {
  22. power = Mathf.Min(power + Time.deltaTime, 1f);
  23. }
  24. // Debug.Log(power);
  25. animator.SetFloat("power", power);
  26. if(firstController.GetArea() && firstController.arrowNum>0){
  27. ClickCheck();
  28. }
  29. }
  30. public void ClickCheck(){
  31. // 按下左键
  32. if (Input.GetMouseButtonDown(0))
  33. {
  34. // Debug.Log("单击左键");
  35. if(!isHolding){
  36. isMouseDown = true;
  37. isMouseLongPressed = false;
  38. // 开始协程检测长按
  39. StartCoroutine(CheckLongPress());
  40. // 触发start
  41. animator.SetFloat("power", power);
  42. animator.SetTrigger("start");
  43. }
  44. else{
  45. ShootAnimator();
  46. }
  47. }
  48. else if(isMouseLongPressed && Input.GetMouseButtonDown(1)){//right key down
  49. // Debug.Log("右键按下");
  50. isHolding = true;
  51. // 停止协程
  52. StopCoroutine(CheckLongPress());
  53. isMouseLongPressed = false;
  54. animator.SetFloat("hold power", power);
  55. animator.SetTrigger("hold");
  56. }
  57. // 松开左键
  58. else if (isMouseDown && Input.GetMouseButtonUp(0))
  59. {
  60. isMouseDown = false;
  61. if(!isHolding){
  62. // Debug.Log("松开左键");
  63. isMouseLongPressed = false;
  64. // 停止协程
  65. StopCoroutine(CheckLongPress());
  66. //触发hold
  67. animator.SetFloat("hold power", power);
  68. // Debug.Log("hold power:"+power+".");
  69. animator.SetTrigger("hold");
  70. // 触发shoot
  71. ShootAnimator();
  72. }
  73. }
  74. }
  75. private IEnumerator CheckLongPress()
  76. {
  77. yield return new WaitForSeconds(longPressDuration);
  78. // 如果鼠标处于按下状态,则表示长按
  79. if (isMouseDown)
  80. {
  81. // Debug.Log("长按左键");
  82. isMouseLongPressed = true;
  83. }
  84. }
  85. private void ShootAnimator(){
  86. animator.SetTrigger("shoot");
  87. firstController.ShootCallback(true, power);
  88. isHolding = false;
  89. power = 0.4f;
  90. }
  91. }

7. 其他部分

        以上就是得分点的实现代码,但是只有这些是不完整的,还有其他比较重要的代码。比如射箭的动作控制:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class CCActionManager : SSActionManager, IActionCallback
  5. {
  6. public FirstController sceneController;
  7. public FlyAction action;
  8. public ArrowFactory factory;
  9. public GameObject arrow;
  10. public GameObject crossbow;
  11. public float forceRate=0.33f;
  12. // Start is called before the first frame update
  13. protected new void Start()
  14. {
  15. sceneController = (FirstController)Director.getInstance().currentSceneController;
  16. sceneController.actionManager = this;
  17. factory = Singleton<ArrowFactory>.Instance;
  18. crossbow = GameObject.Find("Crossbow");
  19. }
  20. public void SSActionEvent(SSAction source,
  21. SSActionEventType events = SSActionEventType.Completed,
  22. int intParam = 0,
  23. string strParam = null,
  24. Object objectParam = null) {
  25. factory.FreeArrow(source.transform.gameObject);
  26. }
  27. public void ShootArrow(float power){
  28. Quaternion bowRotation = crossbow.transform.rotation;
  29. arrow = factory.GetArrow();
  30. Vector3 shootDirection = bowRotation * Vector3.up;
  31. // Debug.Log(shootDirection);
  32. arrow.GetComponent<Rigidbody>().AddForce(shootDirection * power * forceRate, ForceMode.Impulse);
  33. arrow.transform.parent = null;
  34. action = FlyAction.GetSSAction();
  35. this.RunAction(arrow, action, this);
  36. }
  37. }

        还有一个就是前面经常提到的firstController,它是代码的主体控制器,要把它挂载到玩家对象上来实现一系列功能的。

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class FirstController : MonoBehaviour, ISceneController, IUserAction
  5. {
  6. public CCActionManager actionManager;
  7. public ArrowFactory factory;
  8. public UserGUI userGUI;
  9. public GameObject bow;
  10. public ScoreRecorder scoreController;
  11. float shotpower =0.4f;
  12. bool shot = false;
  13. public int arrowNum = 10;
  14. bool inArea = false;
  15. void Awake() {
  16. Director director = Director.getInstance();
  17. bow = GameObject.Find("Crossbow");
  18. director.currentSceneController = this;
  19. director.currentSceneController.LoadSource();
  20. gameObject.AddComponent<UserGUI>();
  21. gameObject.AddComponent<CCActionManager>();
  22. gameObject.AddComponent<ScoreRecorder>();
  23. gameObject.AddComponent<ArrowFactory>();
  24. factory = Singleton<ArrowFactory>.Instance;
  25. userGUI = gameObject.GetComponent<UserGUI>();
  26. }
  27. // Start is called before the first frame update
  28. void Start()
  29. {
  30. }
  31. // Update is called once per frame
  32. void Update()
  33. {
  34. if(inArea && arrowNum>0 ){
  35. Shoot(shotpower);
  36. }
  37. userGUI.arrowNum = arrowNum;
  38. }
  39. public void LoadSource(){
  40. }
  41. public void Shoot(float shootpower){
  42. if(shot){
  43. actionManager.ShootArrow(shootpower);
  44. shot = false;
  45. arrowNum --;
  46. }
  47. }
  48. public void gameOver(){
  49. }
  50. public void ShootCallback(bool isShot, float power)
  51. {
  52. shotpower = power;
  53. shot = isShot;
  54. }
  55. public void AreaCallBack(bool inArea){
  56. this.inArea = inArea;
  57. if(inArea)
  58. arrowNum = 10;
  59. }
  60. public bool GetArea(){
  61. return inArea;
  62. }
  63. }

总结

        上面就是游戏的主体功能的实现了。紧赶慢赶,前前后后用了一周的课余时间才做完的。可以看出游戏比较简陋,代码比较繁琐,而且不能动态地去加载所有的代码和资源;而且准心的实现也很简陋,只是在屏幕上简单的画了一个点等等,还有许多改进的地方。

        作业的完成参考了许多别人的博客,这里感谢所有愿意写博客分享技术的大佬们~

游戏的效果演示:

Unity游戏制作——射箭游戏

        后面应该会整理一下上传到GitHub

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

闽ICP备14008679号