本系列中,我会在0美术的情况下,教大家开发几款简单的小游戏。适合Unity的初学者。
本系列其他游戏开发
今天要开发的游戏是仿造微信跳一跳类型的跳跃游戏。仅有一个场景,简单易学。
首先,我们打开Unity工程,建立一个简单的3d项目,起名叫JumpJump(当然了,这里起名是任意的)
为了体现远近大小一致,我们这里采用正交相机
然后我们就该布置场景了
我们需要一个柱子,让用户能在上面落脚,我们还需要一个玩家,能让用户看到自己的位置,我们的背景也很丑,我们需要一个遮羞布把他挡起来。
为了简单,我这里柱子使用了伸长了的cube,玩家也是一个简单的小cube,遮羞布?那就更简单了,只是个旋转了的plane而已。
在场景中右键选择3D Object 下面的cube和plane我们就创建了如图所示的物体。
然后我们调整一下物体的命名和位置,(这里就不上图了)调整后的结果,相机的位置是(1.12,0,-3)。cube的位置是(0,0,0),缩放是(0.6,1,1),并改名为seat。cube(1)的位置是(0,1,0)缩放是(0.2,0.2,0.5),并改名为player。plane的位置是(0,0,0)旋转是(270,0,0)缩放是(2,1,2),如此一来,我们的场景就布置好了。
注意:在这里检查你的层级关系,这里是没有嵌套的(跟上图一致就对了)
我们发现,我们的plane和方块都是白色,很难看出来个数,所以我们想到,给我们的遮羞布,换个颜色。要想生活过的去,总得头上带点那啥 ,咳咳,那我们就用酷酷的绿色好了。
在下面的assets中右键创建一个材质球
并命名为bg(背景background的缩写)当然了,这里的起名也是随意的。
选中材质球,然后选择颜色,这里颜色当然是任意的了,我就随便选了一个我觉得酷酷的绿色。
选择颜色后,左键选中材质球,并拖拽到plane上,这样我们的plane就是酷酷的绿色了。
我们的小玩家,就是我们的player还没有颜色呢,我们重复上面的材质球步骤,给我们的小玩家,也添上一个颜色。这里我就用了帅帅的黄色。
好了,到这里,我们的布置场景就算结束了。
预制体在Unity里面我们叫它Prefab。我们也可以这样理解:当制作好了游戏组件(场景中的任意一个gameobject我们希望将它制作成一个组件模版,用于批量的套用工作,例如说场景中本质上要重复使用的东西,比如:敌人、士兵、子弹或者一个砖块完全相同的墙体。这里说本质是因为默认生成的prefab其实和模版是一模一样的。
因为我们会有很多很多个落脚点,所以我们需要很多很多的柱子,也就是场景里的seat
我们希望通过预制体这种克隆方式,来生成很多很多的柱子。
选中场景中的seat,然后拖拽其到assets面板中,Unity会自动为我们生成预制体。
如图所示
有了预制体,我们就不需要原始的seat了,我们在场景中删除他
好了,我们的场景已经布置完毕了。接下来,我们只需要编写相应的脚本,我们的游戏就算完成了。
首先,我们需要我们的目的,我们希望,可以让小人跳起来,那么在物理中,我们只需给小人一个向前和向上的力,他就会向前跳起来。小人跳起来会下落,那自然是因为重力的作用,所以为了使小人满足物理世界规律,我们要给小球添加Rigidbody组件。
选中小人,选择Add Component,找到Rigidbody,点击,即可完成添加,此时,小人已经默认有了重力作用。如果此时我们运行游戏,我们的player就会无限往下落。
为了防止我们的遮羞布对我们的游戏造成影响,我们要删除遮羞布的碰撞体
首先,我们在面板中创建一个c#的脚本
起名叫JumpJump(当然了,这里起名也是任意的),双击脚本,打开vs编辑器,如图所示
Unity的脚本很简单,Start是脚本启动的时候运行一次,Update是每帧都会运行,基本上有这俩个方法,我们就能实现一个游戏流程了。
Start方法中,我们要进行一些初始化的工作,就是生成很多个柱子。
要生成很多个柱子,首先我们要获得我们创建的预制体。在Unity脚本中,public变量会显示在Unity的检视面板中,并能为其赋值。所以,我们这里声明我们的预制体变量
public GameObject seat;
然后我们像本文中叙述的挂载材质球一样,将这个脚本挂载在player上,并推拽seat到脚本框内,完成赋值。
完成后如图所示
接着,我们在脚本中编写生成柱子的代码。
我们首先要生成一个柱子,在player的正下方,他可以接住player,作为起始点。
后面的柱子我们希望他们的间隔距离随机的,而且宽度也是不一致的,这样才会有游戏性。很多个柱子我们希望可以动态回收和利用,有助于我们的游戏流畅性提高。
所以,我们需要声明一个变量,来保存我们随机的这些柱子。我这里采用了ArrayList
因为他的动态性比较好。
声明变量:
private ArrayList seats;
然后在Start中编写
//需要new对象
seats = new ArrayList();
//添加第一个柱子在player下方。
seats.Add(Instantiate(seat, new Vector3(0, 0, 0), Quaternion.identity));
//循环添加20个柱子
for (int i = 1; i < 20; i++)
{
//添加的柱子的间隔是随机的
seats.Add(Instantiate(seat, new Vector3(Random.Range(1f, 2.28f) + ((GameObject)seats[i - 1]).transform.position.x, 0, 0), Quaternion.identity));
//修改他们的宽度
((GameObject)seats[i]).transform.localScale = new Vector3(Random.Range(0.5f, 1f), ((GameObject)seats[i]).transform.localScale.y, ((GameObject)seats[i]).transform.localScale.z);
}
柱子有了,我们希望在小人蓄力阶段,柱子可以压缩,而且是缓慢非线形压缩,我们可以自己构建一个数学函数,来表达这个曲线的压缩过程,我们也可以使用Unity给我们提供的数学库中的方法。我这里使用的是Unity的平滑插值smoothstep,他的函数表达式是3x^2 -2x^3。因为压缩柱子是在用户按下屏幕/特定键才会触发松手即回弹,所以我们写在Update方法中,我们还希望我们的程序可以跑在pc和安卓中,所以我们需要检测在pc上按下了特定键,在安卓上是手指触碰了屏幕
(nowat,time和ondown应该声明在程序的最开始,是全局变量,nowat用于指示当前是在哪个柱子上)
if ((Input.GetKey(KeyCode.Space) || Input.touchCount > 0))
{
var y = Mathf.SmoothStep(1, endscalcey, time * 0.01f);
nowat.transform.localScale = new Vector3(nowat.transform.localScale.x, y,nowat.transform.localScale.z);
time += Time.timeScale;
//ondown用来声明是不是已经按下
ondown = true;
//按下时间的最大值
time = time > 100 ? 100 : time;
}
if ((Input.GetKeyUp(KeyCode.Space) || (onandriod && Input.touchCount == 0)) && ondown)
{
ondown = false;
//将按下计时变成0
time = 0;
//回弹
nowat.transform.localScale = new Vector3(nowat.transform.localScale.x, 1, nowat.transform.localScale.z);
}
柱子的压缩我们已经完成了,接下来,我们需要让player可以跳起,因为第一次做游戏,我们并不知道多少的弹跳力合适,所以我们需要在检视面板中可以随时修改,由此,我们声明他为public
public float jump = 1;
添加起跳很简单,我们只需要在用户抬起的一瞬间,我们施加给他一个瞬时的向前和向上的力就可以了,为了简单,我这里假设起跳高度是永恒不变的,time改变的只是向前的力,我们在抬起的代码中添加一行即可
GetComponent().AddForce(new Vector3(time * jump, 300, 0));
我们还需要判断用户是跳在了柱子上,还是掉了下去。也就是判断是否游戏结束。
这个也非常容易,我们只要检测player的y轴坐标是不是小于一个最小值,我们就知道了他是否掉了下去,掉了下去,我们就要重新开始游戏。
在脚本中导入UnityEngine.SceneManagement
然后在Update中编写死亡检测的代码:
if (transform.position.y < 0.2f)
{
//这里要跟场景名称一致
SceneManager.LoadScene("SampleScene");
}
前面讲我们需要压缩柱子,所以我们需要知道压缩的是哪个柱子,我们不希望player可以在空中连续跳跃,所以也要有代码防止这个bug,所以我们使用柱子的碰撞体,撞到哪根柱子,我们就把哪根柱子作为nowat,只有碰到柱子,才可以起跳,离开柱子,不能起跳,所以我们要重写碰撞体的两个方法
private void OnCollisionEnter(Collision collision)
{
nowat = collision.gameObject;
canjump = true;
}
private void OnCollisionExit(Collision collision)
{
canjump = false;
}
我们的代码基本完成了。但是我们希望摄像机和遮羞布一直跟着我们的player移动,而不是摄像机死叮在一个点上,跳跳,看不见我们的player了,这肯定是不和逻辑的。我们也希望回收一些不可见的柱子,提高游戏的流畅性。也要创建新柱子让用户可以接着跳。
为了减轻处理压力,我们一般把摄像机跟随,写在LateUpdate()中。LateUpdate()会在所有的Update方法调用完成后调用。
private void LateUpdate()
{
//player所在位置
Vector3 playerpos = transform.position;
//相机跟随
maincamera.transform.position = new Vector3(playerpos.x + 1.12f, maincamera.transform.position.y, maincamera.transform.position.z);
//遮羞布跟随
plane.transform.position = new Vector3(playerpos.x + 1.12f, plane.transform.position.y, plane.transform.position.z);
//回收柱子
if (playerpos.x > ((GameObject)seats[0]).transform.position.x + 6)
{
((GameObject)seats[0]).SetActive(false);
Destroy(((GameObject)seats[0]));
seats.Remove(seats[0]);
seats.Add(Instantiate(seat, new Vector3(Random.Range(2f, 5f) + ((GameObject)seats[seats.Count - 1]).transform.position.x, Random.Range(-1.09f, 5.53f), -8.2f), Quaternion.identity));
}
}
至此,我们的代码已经写完了,这里附上完整源码。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class JumpJump : MonoBehaviour
{
//预制件
public GameObject seat;
//最终压缩高度
public float endscalcey = 0.5f;
//很多的柱子
private ArrayList seats;
//主相机
public Camera maincamera;
//到哪个柱子了
private GameObject nowat;
//是否可以跳跃
private bool canjump = false;
//按下的时长
private float time = 0;
//指示是否按下
private bool ondown = false;
//弹跳力
public float jump = 1;
//遮羞布
public GameObject plane;
//是否运行在手机,如果运行手机,需要在检视面板中把他勾选上,然后再编译apk
public bool onandriod = false;
void Start()
{
seats = new ArrayList();
seats.Add(Instantiate(seat, new Vector3(0, 0, 0), Quaternion.identity));
for (int i = 1; i < 20; i++)
{
seats.Add(Instantiate(seat, new Vector3(Random.Range(1f, 2.28f) + ((GameObject)seats[i - 1]).transform.position.x, 0, 0), Quaternion.identity));
((GameObject)seats[i]).transform.localScale = new Vector3(Random.Range(0.5f, 1f), ((GameObject)seats[i]).transform.localScale.y, ((GameObject)seats[i]).transform.localScale.z);
}
}
void Update()
{
if (canjump && (Input.GetKey(KeyCode.Space) || Input.touchCount > 0))
{
var y = Mathf.SmoothStep(1, endscalcey, time * 0.01f);
nowat.transform.localScale = new Vector3(nowat.transform.localScale.x, y, nowat.transform.localScale.z);
time += Time.timeScale;
ondown = true;
Debug.Log("asd");
time = time > 100 ? 100 : time;
}
if (canjump && (Input.GetKeyUp(KeyCode.Space) || (onandriod && Input.touchCount == 0)) && ondown)
{
ondown = false;
GetComponent().AddForce(new Vector3(time * jump, 300, 0));
time = 0;
nowat.transform.localScale = new Vector3(nowat.transform.localScale.x, 1, nowat.transform.localScale.z);
}
if (transform.position.y < 0.2f)
{
SceneManager.LoadScene("SampleScene");
}
}
private void LateUpdate()
{
Vector3 playerpos = transform.position;
maincamera.transform.position = new Vector3(playerpos.x + 1.12f, maincamera.transform.position.y, maincamera.transform.position.z);
plane.transform.position = new Vector3(playerpos.x + 1.12f, plane.transform.position.y, plane.transform.position.z);
if (playerpos.x > ((GameObject)seats[0]).transform.position.x + 6)
{
((GameObject)seats[0]).SetActive(false);
Destroy(((GameObject)seats[0]));
seats.Remove(seats[0]);
seats.Add(Instantiate(seat, new Vector3(Random.Range(2f, 5f) + ((GameObject)seats[seats.Count - 1]).transform.position.x, Random.Range(-1.09f, 5.53f), -8.2f), Quaternion.identity));
}
}
private void OnCollisionEnter(Collision collision)
{
nowat = collision.gameObject;
canjump = true;
}
private void OnCollisionExit(Collision collision)
{
canjump = false;
}
}
注意:脚本完成后,需要在检视面板中绑定主相机和遮羞布
完成后如图所示
现在,游戏已经完成了,快让我们运行吧!
点我下载