人人都能写游戏系列(三)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()中更新相机位置,我们还需要相机看向小球。有了这些想法,脚本很容易就写完了。
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,我们的上升浮板就制作完成了。
支持我
您的支持,就是我创作的最大动力