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

引言

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

开始的准备

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

基本代码

控制小球的移动

在平衡球游戏中,小球的移动是重中之重,如果小球不能动,那么我们做出花来也没有用,所以,我们就创建一个名字叫Player的脚本,用来控制小球的移动。
人人都能写游戏系列(三)Unity 3D平衡球游戏
让小球动起来,我们有很多很多种方式,比较常用的是直接操作的小球的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的脚本。
人人都能写游戏系列(三)Unity 3D平衡球游戏
我们希望能在编辑器中调整相机和小球的偏差,而不是每次运行游戏才知道调整的合适不合适,这样有助于我们提升效率。所以我们在脚本的最开始,加上[ExecuteInEditMode],表示,我们不运行的游戏的时候,此脚本也可以工作。
人人都能写游戏系列(三)Unity 3D平衡球游戏
为了跟随小球,我们很明显的需要一个小球的位置,为此,我们可以通过Find函数来寻找场景的中的物体,也可以通过public的方法来拖拽复职,我们这里选用简单的public方式。

public Transform player;

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

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到一个自己觉得合适的值。
人人都能写游戏系列(三)Unity 3D平衡球游戏
运行下游戏,是不是我们的小球已经有模有样了?

制作道具

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

大风车

大风车就是能把小球打飞的那种。因为没有美术,我们就只能女留房号男自强,咳咳,是自己制作了。
我们在场景中创建一个Cylinder(圆柱)
人人都能写游戏系列(三)Unity 3D平衡球游戏
我们更改一下它的长宽高(缩放),让它变成细长条。并沿着x轴旋转90度(放倒)。

人人都能写游戏系列(三)Unity 3D平衡球游戏
同样的,我们再创建一个圆柱,然后调整一下它的旋转,让其为直角,风车的模型就有了。
人人都能写游戏系列(三)Unity 3D平衡球游戏
中间的小球很影响咱们的视野,我们隐藏掉它。
人人都能写游戏系列(三)Unity 3D平衡球游戏
为了方便我们写脚本控制风车,我们创建一个空物体,让这两根圆柱都成为其的子物体。
人人都能写游戏系列(三)Unity 3D平衡球游戏
模型已经ok,我们来写脚本让它旋转起来吧。
创建一个名叫Windmill的脚本。
人人都能写游戏系列(三)Unity 3D平衡球游戏
风车的旋转也很简单,我们只要让他不停的转就可以了,为了保证帧数稳定,我们使用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上,运行游戏,我们就可以看到风车在转动了。
人人都能写游戏系列(三)Unity 3D平衡球游戏
tips:这里我将小球挪了个位置,防止小球在风车内部
运行测试发现,小球确实不能影响风车的转动,风车确实可以影响小球的运动。(如果你觉得风车的长度太短,你也可以自行修改)
修改下风车的命名,并将其拖入下方,成为prefab。
人人都能写游戏系列(三)Unity 3D平衡球游戏
我们第一个道具就完成了!有没有点成就感啊,嘿嘿。

跳板

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

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,将其面积变小,即可完成上述需求。
人人都能写游戏系列(三)Unity 3D平衡球游戏
将跳板更名为Jump,保存为prefab,至此,我们的第二个道具,跳板也完成了。

循环移动的木板

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

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,我们的木板制作,到此结束。
人人都能写游戏系列(三)Unity 3D平衡球游戏

一次性上升踏板

我们这回来制作一次性上升踏板,可以帮助我们的小球上升到更高的位置,这里涉及了两个脚本之间的通讯。
还是回到我们的场景,删除我们的board,然后再添加一个cube。
人人都能写游戏系列(三)Unity 3D平衡球游戏
调整缩放,使其变成一个扁木板,并创建一个小球,赋予小球红色材质,并安放到板子的合适位置上,使小球有所嵌入到板子中,因为这样看起来比较像开关。
人人都能写游戏系列(三)Unity 3D平衡球游戏
接下来,我们要做的就是,当小球触碰开关的时候,浮板会上升,然后隐藏掉开关,制作成一次性上升浮板。
很显然,我们需要这个开关和板子之间的通信,也需要上升的速度,上升的最大高度,这样的参数。
这个上升,我们依然像循环的木板一样,有很多种方案的选择。这里我们先采用刚体的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);
        }
	}
}

这个要怎么赋值?先把开关脚本和上升浮板脚本挂好,然后按图所示。
人人都能写游戏系列(三)Unity 3D平衡球游戏
注意,这里不能从下面的面板中拖拽上去,那样会在内存创建一个新的实例,是不会达到我们的效果的。
我们的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;
    }
}

保存脚本,我们运行游戏,发现报错了
人人都能写游戏系列(三)Unity 3D平衡球游戏
这是因为,我们的上升浮板并没有添加一个rigidbody组件,所以我们将其填上,再运行游戏,我们发现,我们的开关没有消失,小球碰撞到了开关,没能穿过它,这是因为我们的开关的碰撞器没有勾选Is Trigger。
勾选上,再次运行游戏,我们发现我们的小球把浮板砸下去了,这是因为物理组件的特性,所以我们需要锁止浮板的轴。
人人都能写游戏系列(三)Unity 3D平衡球游戏
我们运行发现,我们的板子因为受到小球的重力作用,仍会下降,这是不符合我们的需要的,而且小球不一定可以一瞬间就碰到开关,这样的体验很不好,所以我们采用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组件,调整速度大小,调整命名,最终效果如下
人人都能写游戏系列(三)Unity 3D平衡球游戏
人人都能写游戏系列(三)Unity 3D平衡球游戏
保存为prefab,我们的上升浮板就制作完成了。

支持我

您的支持,就是我创作的最大动力
人人都能写游戏系列(三)Unity 3D平衡球游戏人人都能写游戏系列(三)Unity 3D平衡球游戏