人人都能写游戏系列(三)Unity 3D平衡球游戏

引言

本系列中,我会在0美术的情况下,教大家开发几款简单的小游戏。适合Unity的初学者。
本系列其他游戏开发
今天要开发的就是本系列教程中的第一篇3D游戏教程,过程比较繁琐,我会分几个部分一一讲解。

开始的准备

老规矩,我们来新建一个项目,起名为BalanceBall(平衡球)。
在这里插入图片描述
先来布置一个简单的场景,以方便我们用来测试接下来要做的各种道具。
场景很简单,添加一个我们的核心小球(sphere),一个广阔的场地(plane)。
在这里插入图片描述
接下来我们调整一下小球的位置,大小以及命名,并给小球添加上一个rigidbody组件。
在这里插入图片描述
因为小球是我们的玩家(player),所以我们约定俗成的,给小球设置一个tag,在unity中已经有现成的player的tag了,我们直接选上就可以,这个tag在后面,会有很重要的使用。
在这里插入图片描述
我们的基础准备,到这里就可以了。

基本代码

控制小球的移动

在平衡球游戏中,小球的移动是重中之重,如果小球不能动,那么我们做出花来也没有用,所以,我们就创建一个名字叫Player的脚本,用来控制小球的移动。
在这里插入图片描述
让小球动起来,我们有很多很多种方式,比较常用的是直接操作的小球的position,但是这种方法,没有缓慢加速的功能,小球也不会旋转,会显得很滑,很假。所以我们这里采用更加真实的方法,就好像真实世界中小球受到了重力作用移动了一样。所以我们这里,使用rigidbody来给小球添加外力。
我们可以用Input.GetKey(KeyCode.W);的方式来添加四个判断(wasd),来控制小球的移向,我们也可以用更高级的Axis来控制,Axis可以模拟轴移动,类似摇杆的感觉。
我们还希望能在编辑器中随意控制小球移动的速度,所以还需要一个public的变量。
我们可能会反复使用小球的rigidbody,所以我们为了效率,要把它存储起来。
有了以上的内容,我们就可以写出小球移动的脚本了。

	Rigidbody rigid;
    public float force=5;
    
    void Start () 
    {
        rigid = transform.GetComponent<Rigidbody>();
    }
    
    
    void Update () 
    {
        rigid.AddForce(new Vector3(Input.GetAxis("Horizontal"),0,Input.GetAxis("Vertical"))*force);
    }
   

将脚本挂载到小球上,运行游戏,我们的小球已经可以自由运动了。

更进一步

我们的小球可以运动了,但是它很难停下来,这也是不太合理的。我们希望可以控制它停下来。
那么接下来,我们就要写脚本让小球停下来了。
有同学说了,停住还不容易,只要让小球的速度为0就可以了。
但是我们更希望小球是缓慢停住,而不是突然停住,那我们应该怎么办呢?
答案当然是插值了。我在人人都能写游戏系列(一)Unity简单跳一跳游戏开发一文中使用了平滑插值,用来压缩方块。这里我们当然也可以使用平滑插值,为了学习到更多的内容,我们这回使用mathf中的lerp,线性插值来解决这个问题。
lerp是很简单的,需要三个参数, (float a, float b, float t),a就是起点,b就是终点,t就是0-1的范围,此函数返回float,是(b-a)*t+a的值(就是一次线性函数),有了这些知识,我们就可以动手升级我们的小球脚本了。

完成的小球脚本如下。

using UnityEngine;

public class Player : MonoBehaviour {

   
    Rigidbody rigid;
    public float force=5;
    //用来控制刹车的快慢
    public float reduce = 0.1f;
    void Start () 
    {
       
        rigid = transform.GetComponent<Rigidbody>();
       
    }
    
    
    void Update () 
    {
        rigid.AddForce(new Vector3(Input.GetAxis("Horizontal"),0,Input.GetAxis("Vertical"))*force);
        //按住空格,小球会缓慢减速。
        if(Input.GetKey(KeyCode.Space))
        {
            rigid.velocity = new Vector3(Mathf.Lerp(rigid.velocity.x, 0, reduce),rigid.velocity.y,Mathf.Lerp(rigid.velocity.z, 0, reduce)); 
        }
    }
}

至此,小球的脚本已经完成了。
tips:Input.GetKey和Input.GetKeyDown都可以获取按键的值,区别在于GetKeyDown只触发一次,GetKey会持续触发。

相机跟随

我们在运行游戏的时候发现,我们的小球忽远忽近,感觉很怪,所以我们需要相机始终跟随着小球。
我们很容易就能发现,相机应该始终在小球的后方,相差一个固定的值。
我们创建一个名字叫Track的脚本。
在这里插入图片描述
我们希望能在编辑器中调整相机和小球的偏差,而不是每次运行游戏才知道调整的合适不合适,这样有助于我们提升效率。所以我们在脚本的最开始,加上[ExecuteInEditMode],表示,我们不运行的游戏的时候,此脚本也可以工作。
在这里插入图片描述
为了跟随小球,我们很明显的需要一个小球的位置,为此,我们可以通过Find函数来寻找场景的中的物体,也可以通过public的方法来拖拽复职,我们这里选用简单的public方式。

public Transform player;

为了性能,我们需要保存一下相机自己的位置。
为了方便调整,我们需要一个public的变量,用来表示相机到小球的距离,我们通常在LateUpdate()中更新相机位置,我们还需要相机看向小球。有了这些想法,脚本很容易就写完了。

扫描二维码关注公众号,回复: 10910129 查看本文章
using UnityEngine;
[ExecuteInEditMode]
public class Track : MonoBehaviour 
{
    public Transform player;
    Transform trans;
    public Vector3 dis;
	// Use this for initialization
	void Start () 
    {
        trans = this.transform;
      
	}
	void LateUpdate()
	{
        trans.position = player.position+dis;
        trans.LookAt(player);
	}
}

将脚本挂载到相机上,然后在编辑器面板中调整dis到一个自己觉得合适的值。
在这里插入图片描述
运行下游戏,是不是我们的小球已经有模有样了?

制作道具

光有个小球,我们还是什么都干不了的,游戏的魅力在于就是有各种各样的关卡和道具,在平衡球游戏中,主要就是道具了。下面我们就来制作各种各样的道具。

大风车

大风车就是能把小球打飞的那种。因为没有美术,我们就只能女留房号男自强,咳咳,是自己制作了。
我们在场景中创建一个Cylinder(圆柱)
在这里插入图片描述
我们更改一下它的长宽高(缩放),让它变成细长条。并沿着x轴旋转90度(放倒)。

在这里插入图片描述
同样的,我们再创建一个圆柱,然后调整一下它的旋转,让其为直角,风车的模型就有了。
在这里插入图片描述
中间的小球很影响咱们的视野,我们隐藏掉它。
在这里插入图片描述
为了方便我们写脚本控制风车,我们创建一个空物体,让这两根圆柱都成为其的子物体。
在这里插入图片描述
模型已经ok,我们来写脚本让它旋转起来吧。
创建一个名叫Windmill的脚本。
在这里插入图片描述
风车的旋转也很简单,我们只要让他不停的转就可以了,为了保证帧数稳定,我们使用FixedUpdate。
这里的转,我们使用Transform的Rotate方法。为什么不使用刚体旋转?因为我们希望风车在和小球发生碰撞时,也不会减速和跳起。
经过本系列前面的教程,这种脚本应该对你来说很简单了。

using UnityEngine;

public class Windmill : MonoBehaviour {

    Transform trans;
    public float speed = 30;
	void Start () 
	{
        trans = this.transform;
	}
	
	
	void FixedUpdate()
	{
        trans.Rotate(Vector3.up * speed * Time.fixedDeltaTime);
	}
}

将脚本挂载到风车的父物体的GameObject上,运行游戏,我们就可以看到风车在转动了。
在这里插入图片描述
tips:这里我将小球挪了个位置,防止小球在风车内部
运行测试发现,小球确实不能影响风车的转动,风车确实可以影响小球的运动。(如果你觉得风车的长度太短,你也可以自行修改)
修改下风车的命名,并将其拖入下方,成为prefab。
在这里插入图片描述
我们第一个道具就完成了!有没有点成就感啊,嘿嘿。

跳板

跳板是小球踩上去以后,会瞬间的跳起来的一个道具。
借由跳板,我们的小球就可以跳上更高的平台。
还是先来制作跳板。跳板的模型选择可以有多种多样,为了更广的知识面,我们选用Quad(四边形)。
在这里插入图片描述
为了和地面区分开来,我们给跳板挂载一个红色的材质。
在这里插入图片描述
接着,调整四边形,让它躺在地上的合适位置,方便我们的测试。
在这里插入图片描述
我们采用触碰触发的方式触发跳跃,而不是碰撞的方式触发跳跃,所以要勾选上如图所示内容。
在这里插入图片描述
至此,跳板的模型,我们就做好了,接下来来写跳板的脚本。
跳板的脚本也很简单,主要是检测触发器被触发,然后施加给小球一个瞬间的力,就可以让小球跳起了。

using UnityEngine;

public class Jump : MonoBehaviour {

    public float force=10;
	
	private void OnTriggerEnter(Collider other)
	{
        if(other.CompareTag("Player"))
        {
            other.GetComponent<Rigidbody>().AddForce(Vector3.up * force,ForceMode.Impulse);
        }
	}
}

这里就使用了tag标签,只有当小球触碰触发器的时候才会跳跃,别的物体触碰它并不会被弹起。

更进一步

我们发现,小球在刚刚进入红色区域的时候就已经被弹起了,我们更希望小球完全进入红色区域在被弹起,那我们应该怎么办呢?
这个问题有两种办法,第一种是写脚本,计算小球中心到平面的距离,第二种就是修改触发器的面积。
这里采用第二种方法,把触发器的面积变小,自然小球就能更好的更自然的弹起来了。
删除mesh collider,添加box collider,将其面积变小,即可完成上述需求。
在这里插入图片描述
将跳板更名为Jump,保存为prefab,至此,我们的第二个道具,跳板也完成了。

循环移动的木板

在很多游戏中,就存在循环移动的木板,把玩家从一岸移动到另一岸。这里,我们也创建一个可以循环移动的木板。
还是先来场景中创建我们的板子,当然了,这里用cube最方便了。
在这里插入图片描述
我们的测试场景中已经有很多东西了,我们已经保存了prefab,就可以删除一些不必要的东西了。我们把风车 跳板 大地都删除掉,让我们的player占在方块上。
在这里插入图片描述
然后在创建一个方块,表示成河对面。
在这里插入图片描述
在创建一个方块,将其拍扁,它就是我们的木板了。
在这里插入图片描述
场景有了,我们来写脚本吧。
我们不希望直接在脚本中为移动距离赋值,因为我们更希望我们的板子可以通用,不受到河岸距离的限制。
那我们应该怎么做呢?
很简单,检测碰撞!只要碰到河岸就换方向
这样我们就可以实现了更广泛的适配。
脚本如下:

using UnityEngine;

public class Board : MonoBehaviour {

    Transform trans;
    //前进方向
    int dir = 1;
    public float speed = 1;
	void Start () 
	{
        trans = this.transform;
	}
	
	void FixedUpdate () 
	{
        trans.position += Vector3.forward * speed*Time.fixedDeltaTime*dir;
	}

	private void OnCollisionEnter(Collision collision)
	{
        if(!collision.gameObject.CompareTag("Player"))
        dir = -dir;
	}
}

这样,我们的循环移动的木板也做好了。

更进一步

我们运行发现,板子移动速度太快,我们的小球很难在板子上站稳,直接操作position更是无摩擦的抽板子,是在是地狱式难度。为了降低难度,为了和物理世界规律相同,我们将移动的代码改为使用rigidbody下的MovePosition。于是,脚本变成了如下所示:

using UnityEngine;

public class Board : MonoBehaviour {
    
    Rigidbody rigid;
    //前进方向
    int dir = 1;
    public float speed = 1;
	void Start () {
        rigid = GetComponent<Rigidbody>();
	}
	
	void FixedUpdate () {
        rigid.MovePosition(rigid.position + Vector3.forward * speed * Time.fixedDeltaTime * dir);
	}

	private void OnCollisionEnter(Collision collision)
	{
        if(!collision.gameObject.CompareTag("Player"))
        dir = -dir;
	}
}

经过测试发现,我们的小球站上去后,给了板子一个摩擦力,板子会运行的飞快,这是不合理的,所以我们这回不在移动位置,只控制移动的速度。

using UnityEngine;

public class Board : MonoBehaviour {
    
    Rigidbody rigid;
    //前进方向
    int dir = 1;
    public float speed = 1;
	void Start () {
        rigid = GetComponent<Rigidbody>();
	}
	
	void FixedUpdate () {
        rigid.velocity = Vector3.forward * speed * Time.fixedDeltaTime * dir;
	}

	private void OnCollisionEnter(Collision collision)
	{
        if(!collision.gameObject.CompareTag("Player"))
        dir = -dir;
	}
}

我们给木板添加上一个材质球,更改一下其颜色,与河岸分割开来,更名并保存为prefab,我们的木板制作,到此结束。
在这里插入图片描述

一次性上升踏板

我们这回来制作一次性上升踏板,可以帮助我们的小球上升到更高的位置,这里涉及了两个脚本之间的通讯。
还是回到我们的场景,删除我们的board,然后再添加一个cube。
在这里插入图片描述
调整缩放,使其变成一个扁木板,并创建一个小球,赋予小球红色材质,并安放到板子的合适位置上,使小球有所嵌入到板子中,因为这样看起来比较像开关。
在这里插入图片描述
接下来,我们要做的就是,当小球触碰开关的时候,浮板会上升,然后隐藏掉开关,制作成一次性上升浮板。
很显然,我们需要这个开关和板子之间的通信,也需要上升的速度,上升的最大高度,这样的参数。
这个上升,我们依然像循环的木板一样,有很多种方案的选择。这里我们先采用刚体的MovePosition看看
先来写开关的代码

using UnityEngine;

public class Switch : MonoBehaviour {

	private void OnTriggerEnter(Collider other)
	{
        if(other.gameObject.CompareTag("Player"))
        {
            //通知板子上升

            //隐藏自己
            this.gameObject.SetActive(false);
        }
	}
}

再来写板子上升的代码

using UnityEngine;

public class UpBoard : MonoBehaviour 
{

    Rigidbody rigid;
    //指示是否要上升
    bool canup = false;
    //1向上走,-1向下走
    public int dir = 1;
    //上升速度
    public float speed = 20;
    //最大上升高度
    public float max = 10;
    //记录最开始的y位置
    float startY;
	void Start () 
    {
        rigid = GetComponent<Rigidbody>();
        startY = rigid.position.y;
	}

	void FixedUpdate()
	{
        if (!canup)
            return;
        rigid.MovePosition(rigid.position +Vector3.up*dir*speed*Time.fixedDeltaTime);
        if (rigid.position.y - startY >= max)
            canup = false;
	}
    //用于和开关通信
	public void MoveUp()
    {
        
    }
}

现在我们来写两个脚本通信的方法,这件事情上我们也有很多种选择,比如共用一个全局变量,或者直接Find到物体,再通过GetComponent找到脚本,再修改其中的值,直接用public赋值等等。
我们这里遵循简单原则,选择public赋值的方法。那么我们先补全开关的脚本。

using UnityEngine;

public class Switch : MonoBehaviour {
    //在面板赋值,需要注意,必须是挂载的物体。
    public UpBoard up;
	private void OnTriggerEnter(Collider other)
	{
        if(other.gameObject.CompareTag("Player"))
        {
            //通知板子上升
            up.MoveUp();
            //隐藏自己
            this.gameObject.SetActive(false);
        }
	}
}

这个要怎么赋值?先把开关脚本和上升浮板脚本挂好,然后按图所示。
在这里插入图片描述
注意,这里不能从下面的面板中拖拽上去,那样会在内存创建一个新的实例,是不会达到我们的效果的。
我们的moveup还是空方法,我们需要补全他。
观看我们的脚本可以发现,我们只需要将canup赋值为true,即可让脚本上升了,所以,完整的浮板脚本如下。

using UnityEngine;

public class UpBoard : MonoBehaviour 
{

    Rigidbody rigid;
    //指示是否要上升
    bool canup = false;
    //1向上走,-1向下走
    public int dir = 1;
    //上升速度
    public float speed = 20;
    //最大上升高度
    public float max = 10;
    //记录最开始的y位置
    float startY;
	void Start () 
    {
        rigid = GetComponent<Rigidbody>();
        startY = rigid.position.y;
	}

	void FixedUpdate()
	{
        if (!canup)
            return;
        rigid.MovePosition(rigid.position +Vector3.up*dir*speed*Time.fixedDeltaTime);
        if (rigid.position.y - startY >= max)
            canup = false;
	}
    //用于和开关通信
	public void MoveUp()
    {
        canup = true;
    }
}

保存脚本,我们运行游戏,发现报错了
在这里插入图片描述
这是因为,我们的上升浮板并没有添加一个rigidbody组件,所以我们将其填上,再运行游戏,我们发现,我们的开关没有消失,小球碰撞到了开关,没能穿过它,这是因为我们的开关的碰撞器没有勾选Is Trigger。
勾选上,再次运行游戏,我们发现我们的小球把浮板砸下去了,这是因为物理组件的特性,所以我们需要锁止浮板的轴。
在这里插入图片描述
我们运行发现,我们的板子因为受到小球的重力作用,仍会下降,这是不符合我们的需要的,而且小球不一定可以一瞬间就碰到开关,这样的体验很不好,所以我们采用MovePosition的方案失败了。所以这里我们采用直接移动板子的方法。
完整的上升浮板代码如下

using UnityEngine;

public class UpBoard : MonoBehaviour 
{

    Transform trans;
    //指示是否要上升
    bool canup = false;
    //1向上走,-1向下走
    public int dir = 1;
    //上升速度
    public float speed = 1;
    //最大上升高度
    public float max = 10;
    //记录最开始的y位置
    float startY;
	void Start () 
    {
        trans = this.transform;
        startY = trans.position.y;
	}

	void FixedUpdate()
	{
        if (!canup)
            return;
        trans.position += Vector3.up * dir * speed * Time.fixedDeltaTime;
        if (trans.position.y - startY >= max)
            canup = false;
	}
    //用于和开关通信
	public void MoveUp()
    {
        canup = true;
    }
}

删除掉上升浮板的rigidbody组件,调整速度大小,调整命名,最终效果如下
在这里插入图片描述
在这里插入图片描述
保存为prefab,我们的上升浮板就制作完成了。

火炮

这是本篇教程中最难的部分,我们还是先来到场景中,制作一个大炮。
先将cube的x拉长,方便我们摆放大炮。
在这里插入图片描述
我们知道,大炮是由两个轮子,一个炮筒组成的。
这三个部分都可以用圆柱体来代替,所以,我们就创建三个圆柱体。把两个改成扁扁的轮子,一个改成长长的炮筒。
这就是我造的轮子了。
在这里插入图片描述
大家可以按这个参数造轮子,也可以按自己觉得合适的大小来。
炮管的参数
在这里插入图片描述
这里我觉得刚才的轮子太小了,又进行了一波修改,反正只要看起来差不多就行了,毕竟这种东西,也没有什么定数。
将两个轮子摆在炮筒旁边,我们就完成了大炮的基本构建。
附上新的轮子参数
在这里插入图片描述
我们知道,unity在操作物体的时候,是按物体的mesh中心来的,圆柱体的mesh中心在圆柱的中间。
而我们希望操作大炮是操作圆柱的底部,因为操作中间,当炮筒变成90度的时候,很可能就离开轮子了,那看起来太假了。
我们知道,操作父物体的时候,子物体会跟随父物体的变化而变化,所以这个问题就很容易解决了,我们直接在炮筒的底部,添加一个空物体,作为炮管的父物体,这样,操作这个空物体的transform,炮筒就会跟着动了。
先创建物体,将物体摆放在圆柱的底部,具体做法是,x,z坐标和圆柱一致,只修改y值,就可以保证是在圆柱底部的中央了。
在这里插入图片描述
接着,把炮筒挂为其的子物体。这里直接拖拽炮筒就可以了。
在这里插入图片描述
我们还需要一个发射点,就是小球是从哪里发射出去的,显然,这应该是在炮筒的顶部,而且随着炮筒的移动而移动,根据父物体移动子物体会跟随的道理,我们再创建一个空物体,调整好位置后,挂载到炮筒上,变成炮筒的子物体。
在这里插入图片描述
这里要注意,我们必须把发射点离炮筒远一些,因为小球有半径,如果不够远,小球会卡在炮筒里无法发射。
这个位置也是要经过实际测试才会知道,现在我就先把它摆在这里了。
为了方便管理大炮,我们还需要创建一个空物体,作为整个大炮的父物体。
在这里插入图片描述
然后,我们删除掉大炮子物体的所有碰撞体,我们来自己给大炮做一个碰撞模型。
在这里插入图片描述
现在大炮的碰撞体已经全部删除了,我们给大炮更改下命名,方便观察和说明。
在这里插入图片描述
我们给Cannon添加上一个Box Collider。
在这里插入图片描述
然后点击,尽可能的调整碰撞碰撞范围,使碰撞体紧贴大炮。
在这里插入图片描述
我调整的效果如下
在这里插入图片描述
好了,大炮已经造好,创建一个名为Cannon的脚本,我们可以开始写代码了。
在这里插入图片描述
这里的思路是,当小球碰撞到了大炮,小球就进入大炮,然后我们可以调整炮管的角度,按下指定键,就可以发射了。
有几个关键的地方需要注意,首先,小球未和大炮发生碰撞,我们不能操作大炮。其次,如何体现出小球进入了大炮,再然后,如何让摄像机跟随大炮。这几个想明白了,脚本就很好写了。
先来写碰撞检测和如何移动大炮的角度吧。

using UnityEngine;

public class Cannon : MonoBehaviour {
    //移动点的位置。
    public Transform trans;
    Transform player;
    //炮管的位置。
    public Transform gun;
    //移动炮管的速度
    public float speed=20;
	void Start () 
    {
       
	}
	
	void Update () 
    {
        //表示没碰到大炮时,大炮不会被操作。
        if (player == null)
            return;
        trans.Rotate(Vector3.right * speed * Time.deltaTime * Input.GetAxis("Vertical"));
	}

	void OnCollisionEnter(Collision collision)
	{
        if(collision.gameObject.CompareTag("Player"))
        {
            player = collision.gameObject.transform;
            player.gameObject.SetActive(false);
            //这里用了取巧的办法,让摄像机看向大炮。
            player.position = gun.position;
        }
	}
}

将脚本挂载到Cannon上,拖过拖拽赋值,我们运行游戏,发现脚本很好的运行了。
在这里插入图片描述
接下来,我们来写如何让大炮发射小球。
为了和前面的按键区分开来,我们这里使用g键发射。

using UnityEngine;

public class Cannon : MonoBehaviour {
    //移动点的位置。
    public Transform trans;
    Transform player;
    //炮管的位置。
    public Transform gun;
    //发射点的位置
    public Transform shoot;
    //移动炮管的速度
    public float speed=20;
    //发射力
    public float force = 10;
	void Start () 
    {
       
	}
	
	void Update () 
    {
        //表示没碰到大炮时,大炮不会被操作。
        if (player == null)
            return;
        trans.Rotate(Vector3.right * speed * Time.deltaTime * Input.GetAxis("Vertical"));
        if(Input.GetKeyDown(KeyCode.G))
        {
            //让小球到发射点位置
            player.position = shoot.position;
            //让小球可见
            player.gameObject.SetActive(true);
            //施加一个瞬时力,方向是移动点的y轴朝向。
            player.GetComponent<Rigidbody>().AddForce(force*trans.up,ForceMode.Impulse);
        }
	}

	void OnCollisionEnter(Collision collision)
	{
        if(collision.gameObject.CompareTag("Player"))
        {
            player = collision.gameObject.transform;
            player.gameObject.SetActive(false);
            //这里用了取巧的办法,让摄像机看向大炮。
            player.position = gun.position;
        }
	}
}

保存代码,在场景赋值,调整好角度,按下g,就可以发射了。
在这里插入图片描述

更进一步

我们运行游戏会发现,大炮可以360度旋转,会插到地面中去,所以,我们要调整大炮所能旋转的角度。比如最基本的,我们让大炮只能0-90度旋转。
那我们应该怎么做呢?
有同学说了,这还不简单?只要判断transform.rotation.x就可以了。
事实是这样么?
在transform的文档中是这么写的
在这里插入图片描述
这个返回的是Quaternion,并不是所谓的角度。
那么Quaternion,又是什么呢?
文档中说了

四元数用于表示旋转。它们结构紧凑,不受万向节锁定的影响,可以轻松插补。 Unity内部使用四元数来表示所有旋转。

四元数是一个很不容易理解的概念。它的出现是为了解决欧拉旋转中的万向锁问题。具体的更多的关于四元数的内容,大家可以自行百度,这里因为篇幅原因,只讲如何使用。
四元数和欧拉角的互化,有着一套极为繁琐的公式,不过别担心,unity引擎已经为我们做好了这套转化工作。
通过四元数的eulerAngles方法,我们就可以轻松获取到xyz三轴的旋转角了。
有了这些知识,我们控制旋转角度看似就很简单了。

using UnityEngine;

public class Cannon : MonoBehaviour {
    //移动点的位置。
    public Transform trans;
    Transform player;
    //炮管的位置。
    public Transform gun;
    //发射点的位置
    public Transform shoot;
    //移动炮管的速度
    public float speed=20;
    //发射力
    public float force = 10;
	void Start () 
    {
      
	}
	
	void Update () 
    {
        //表示没碰到大炮时,大炮不会被操作。
        if (player == null)
            return;
        //控制旋转
        Quaternion a = trans.rotation;
        if (a.eulerAngles.x >= 90 && Input.GetAxis("Vertical") >= 0)
            return;
        if(a.eulerAngles.x <= 0 && Input.GetAxis("Vertical") <= 0)
            return;
        trans.Rotate(Vector3.right * speed * Time.deltaTime * Input.GetAxis("Vertical"));
        if(Input.GetKeyDown(KeyCode.G))
        {
            //让小球到发射点位置
            player.position = shoot.position;
            //让小球可见
            player.gameObject.SetActive(true);
            //施加一个瞬时力,方向是移动点的朝向。
            player.GetComponent<Rigidbody>().AddForce(force*trans.up,ForceMode.Impulse);
        }
	}

	void OnCollisionEnter(Collision collision)
	{
        if(collision.gameObject.CompareTag("Player"))
        {
            player = collision.gameObject.transform;
            player.gameObject.SetActive(false);
            //这里用了取巧的办法,让摄像机看向大炮。
            player.position = gun.position;
        }
	}
}

我们保存脚本,回到场景中运行代码,我们发现,代码并没有按我们想象中的那样生效,这是为什么呢?
答案很简单,那是因为四元数返回的角度是从0-360度的,不存在负角,而场景中,则是正负0-180的,所以我们需要一步转化,将角度转化成我们面板看到的那样。

  float changeAngle(float angle)
    {
        if (angle > 180)
            return angle - 360;
        return angle;
    }

有了这个,我们在把角度放到里面转化一下,这样子,我们的脚本就貌似完成了。

using UnityEngine;

public class Cannon : MonoBehaviour {
    //移动点的位置。
    public Transform trans;
    Transform player;
    //炮管的位置。
    public Transform gun;
    //发射点的位置
    public Transform shoot;
    //移动炮管的速度
    public float speed=20;
    //发射力
    public float force = 10;
	void Start () 
    {
      
	}
	
	void Update () 
    {
        //表示没碰到大炮时,大炮不会被操作。
        if (player == null)
            return;
        Quaternion a = trans.rotation;
       //转化角度
        if (changeAngle(a.eulerAngles.x) >= 90 && Input.GetAxis("Vertical") >= 0)
            return;
        if(changeAngle(a.eulerAngles.x) <= 0 && Input.GetAxis("Vertical") <= 0)
            return;
        trans.Rotate(Vector3.right * speed * Time.deltaTime * Input.GetAxis("Vertical"));
        if(Input.GetKeyDown(KeyCode.G))
        {
            //让小球到发射点位置
            player.position = shoot.position;
            //让小球可见
            player.gameObject.SetActive(true);
            //施加一个瞬时力,方向是移动点的朝向。
            player.GetComponent<Rigidbody>().AddForce(force*trans.up,ForceMode.Impulse);
        }
	}

	void OnCollisionEnter(Collision collision)
	{
        if(collision.gameObject.CompareTag("Player"))
        {
            player = collision.gameObject.transform;
            player.gameObject.SetActive(false);
            //这里用了取巧的办法,让摄像机看向大炮。
            player.position = gun.position;
        }
	}

    float changeAngle(float angle)
    {
        if (angle > 180)
            return angle - 360;
        return angle;
    }
}

我们再次运行发现,它有时候会生效,让炮筒无法旋转,有时候不会卡住,直接转到了底部。这是为什么呢?
通过Debug发现,a.eulerAngles.x返回的角在正方向上是0-90-0的过程。由于90为极限值,用户按住w时候,由于大炮速度过快,很容易就超过了90,并没有触发a.eulerAngles.x >= 90这个判断,而到底部之后,a.eulerAngles.x开始返回大于180度的角,经过转化,会触发changeAngle(a.eulerAngles.x) <= 0 这个判断,导致大炮卡住。如果在最开始,直接向后运动,会直接触发changeAngle(a.eulerAngles.x) <= 0 这个判断卡住,也就是说,我们的问题就在于90度这个位置。我们应该怎么办呢?
很简单,还是利用四元数,这回我们不是直接返回a.eulerAngles.x的角度去判断,而是通过a和世界正上方向的夹角来判断,这样,我们在正方向上也会返回大于90度的值,这个问题就解决了。

最后修改后的大炮代码如下:

using UnityEngine;

public class Cannon : MonoBehaviour {
    //移动点的位置。
    public Transform trans;
    Transform player;
    //炮管的位置。
    public Transform gun;
    //发射点的位置
    public Transform shoot;
    //移动炮管的速度
    public float speed=20;
    //发射力
    public float force = 10;
	void Start () 
    {
      
	}
	
	void Update () 
    {
        //表示没碰到大炮时,大炮不会被操作。
        if (player == null)
            return;
        Quaternion a = trans.rotation;
        Quaternion c = Quaternion.Euler(Vector3.up);
        //c是世界的正上方向,通过判断ac夹角,我们来判断是否超过了90度。
        if (changeAngle(Quaternion.Angle(a,c)) >= 90 && Input.GetAxis("Vertical") >= 0)
            return;
        if(changeAngle(a.eulerAngles.x) <= 0 && Input.GetAxis("Vertical") <= 0)
            return;
        //既然都用了四元数了,我们也把旋转角改成四元数的模式吧!
        Quaternion b = Quaternion.Euler(Vector3.right * speed * Time.deltaTime * Input.GetAxis("Vertical"));
        trans.rotation = a * b;
        if(Input.GetKeyDown(KeyCode.G))
        {
            //让小球到发射点位置
            player.position = shoot.position;
            //让小球可见
            player.gameObject.SetActive(true);
            //施加一个瞬时力,方向是移动点的朝向。
            player.GetComponent<Rigidbody>().AddForce(force*trans.up,ForceMode.Impulse);
        }
	}

	void OnCollisionEnter(Collision collision)
	{
        if(collision.gameObject.CompareTag("Player"))
        {
            player = collision.gameObject.transform;
            player.gameObject.SetActive(false);
            //这里用了取巧的办法,让摄像机看向大炮。
            player.position = gun.position;
        }
	}

    float changeAngle(float angle)
    {
        if (angle > 180)
            return angle - 360;
        return angle;
    }
}

小结

到此为止,我们的道具就制作完成了,由于篇幅原因,进一步的关卡/死亡/通关等判断和布置,请期待下一篇章。

支持我

您的支持,就是我创作的最大动力

发布了28 篇原创文章 · 获赞 49 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qwe25878/article/details/86481920
今日推荐