前言
[Unity]《太空射击》开发日记Ep.1(入门篇)
在上一篇日记中,我从最最基本的一些操作开始,抛弃了美工、架构等等待优化的方面,很直接的实现了一个键盘控制主角的效果。这篇日记会在上篇的基础上更加深入一些,要把基础用的更熟练一点。
PS:下面出现的所有动图都是偏快的!
敌人1脚本
Enemy1我想让他有自己的意识——朝一个随机方向移动
有了上次的脚本作为参照,这个功能看起来就没那么复杂了。不过一切还是要按部就班的来:
新建脚本enemy1MoveController
,添加类成员,实现Update()函数。
public class enemy1MoveController : MonoBehaviour
{
public float moveSpeed;
private float xAngle;
private float yAngle;
// Use this for initialization
void Start()
{
//随机获取弧度
float rad = Random.Range(0.0f, 2 * Mathf.PI);
xAngle = 1 * Mathf.Cos(rad);
yAngle = 1 * Mathf.Sin(rad);
}
// Update is called once per frame
void Update()
{
transform.Translate(new Vector2(xAngle, yAngle) * moveSpeed * Time.deltaTime);
}
}
代码说明:
1.只有public的类成员才会在Unity中作为一个属性显示出文本框供赋值。
2.Start()是附着的对象生成时执行一次的函数,一般用来初始化。
3.Random.Range(float min, float max):返回一个[min,max]的随机数,因为传入了float,所以返回的也是float。
4.底下的Vector2(x,y)实际上是一个表示方向的单位向量(cosθ,sinθ),相当于0~360°随机方向。
但如果Enemy1在屏幕边缘生成的话,随机到往屏幕外的方向怎么办
事实上我们想要的理想结果是,Enemy1随机地从某个边缘进入屏幕,然后朝对面直线飞行。
所以我们还需初始化Enemy1的位置,并且生成对应的合理的随机角度。
public float moveSpeed;
private float xAngle;
private float yAngle;
private float xPos;
private float yPos;
// Use this for initialization
void Start()
{
float rad=0;
//{0,1,2,3}={右边缘,上边缘,左边缘,下边缘}
int direction = Random.Range(0, 4);
switch (direction)
{
case 0:
//90~270°
rad = Random.Range(0.5f * Mathf.PI, 1.5f * Mathf.PI);
xPos = 5;
yPos = Random.Range(-5.0f, 5.0f);
break;
case 1:
//180~360°
rad = Random.Range(1.0f * Mathf.PI, 2f * Mathf.PI);
xPos = Random.Range(-5.0f, 5.0f);
yPos = 5;
break;
case 2:
//270~450°
rad = Random.Range(1.5f * Mathf.PI, 2.5f * Mathf.PI);
xPos = -5;
yPos = Random.Range(-5.0f, 5.0f);
break;
case 3:
//0~180°
rad = Random.Range(0.0f, Mathf.PI);
xPos = Random.Range(-5.0f, 5.0f);
yPos = -5;
break;
}
xAngle = 1 * Mathf.Cos(rad);
yAngle = 1 * Mathf.Sin(rad);
//位置初始化
transform.position = new Vector2(xPos, yPos);
}
虽然这样子无法避免Enemy1朝侧边飞行的情况,但起码Enemy1一定是朝屏幕内飞行的。以后可以再对Enemy1进行完善。
预制件
只有一个Enemy1没什么难度,一定要弄很多个一起攻击主角才有趣
Unity中可以把一个精灵当作一个文件来保存,称为预制件(Prefab),相当于一个克隆体。并且只要母体改变,子克隆体也会随之改变,很适合长期保存修改和动态生成游戏对象。
预制件的文件后缀为.prefab,我们同样用一个Prefabs
文件夹来管理所有的预制件
,并且为了后续的开发,我们要调整一下项目文件夹结构:先在根目录创建一个Resources
文件夹,再在里面自定义我们自己的文件夹。新的文件目录就长这样
剩下的就很简单,直接把Player和Enemy1拖到Prefabs
中,他们会自动变成预制件。
如何用预制件来生成一堆Enemy1呢
我们可以利用场景里必存在的Camera做个小试验,给他附加一个生成Enemy1克隆的脚本。
新的脚本叫做enemy1BornController
,添加到摄像机(Main Camera)上,代码简单的出乎意料:
public class enemy1BornController : MonoBehaviour {
public GameObject pre_Enemy1;
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
Instantiate(pre_Enemy1);
}
}
代码说明:
1.公共的GameObject同样可以在Unity中进行赋值,就用刚做好的Enemy1.prefab。
运行后大概是这个效果。
敌人1的完善工作
上面的小试验可以看出Enemy1还存在诸多问题。
1.在与Player碰撞后,应该销毁Player和自身(暂定只有一条生命)。
2.克隆体对象飞出屏幕后一直存在于游戏中,引起资源浪费。
3.(之前待解决的)飞行轨道凌乱不堪,应朝对边飞行而不是左右边。
4.在克隆体实验成功后,思考如何生成一群合理的敌人。
这些问题或早或晚都是要解决的。
触发器式碰撞
主角碰到Enemy1之后,应该要受到伤害
这个效果的核心在于检测"碰撞",这需要我们给Player和Enemy1定义有效碰撞区域。当两个区域重叠后,就视为发生碰撞。
第一步,给两个预制件添加碰撞区域。
在Inspector窗口中,选择Add Component>Physics 2D>Box Collider 2D
。因为我们实际上不需要Enemy1间的互相碰撞,所以把Enemy1的Box Collier的is Trigger勾选上,不然会出现Enemy1之间互相排挤的现象。
第二步,给两个预制件添加刚体组件。
在Inspector窗口中,选择Add Component>Physics 2D>Rigidbody 2D
。并且因为我们是在太空,所以是不受重力影响的,因此要把Linear Drag, Angular Drag, Gravity Scale都赋为0。
第三步,将Player的Tag属性设置为Player,将Enemy1的Tag属性设置为Enemy。
这个也在Inspector窗口的最顶上,是一个下拉菜单框的样子。
第四步,编写并添加碰撞脚本。
新脚本名为playerTrigger
,添加到Player上。
public class playerTrigger : MonoBehaviour {
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
}
private void OnTriggerEnter2D(Collider2D other)
{
if(other.gameObject.tag=="Enemy")
{
Destroy(other.gameObject);
Destroy(gameObject);
}
}
}
代码说明:
1.OnTriggerEnter2D表示该gameObject的碰撞体(Collider)碰到了other.gameObject的触发器(Collider & is Trigger==True),在此例中就是Player的BoxCollier碰到了Enemy1的Trigger BoxCollider。
2.判定tag属性是一种方式,也可以直接判定name属性。
3.Destroy是销毁游戏实例,注意要先销毁对方,再销毁自己。
运行游戏,现在Player碰到Enemy1后两者都会被销毁。
屏幕边缘检测
Enemy1飞出屏幕之后还会一直存在,如何及时销毁啊
这时候就有个很巧妙的办法,利用我们刚刚掌握的碰撞机制,在屏幕外围上四堵空气墙(不要盖到Enemy的生成区),然后只要有Enemy碰到空气墙,就会被销毁。
为了追求精简,我们先在场景中新建一个左边的空气墙。在Hierachy窗口中,右键Create Empty
。
然后给这个对象重命名EdgeBox并Reset坐标。
然后先添加左侧的Box Collider 2D,我打算给左侧预留1个unit的空间,勾选好is Trigger并设置参数Offset(-6.5,0),Size(1,12)。Offset指的是碰撞体距离对象位置的偏移量,Size指的是碰撞体的方块尺寸。
仿照这个参数,我们再添加3堵另外3个方向的空气墙(Box Collider 2D),也就是说可以用多个碰撞体叠加成一个更大的碰撞体。
最后添加一个静态的的Rigibody 2D即可。(Body Type为Static,意思是一动不动)
效果如下图:
有了碰撞体之后,最后只要添加一个销毁脚本就行了。
在enemy1MoveController
里添加碰撞函数:
private void OnTriggerEnter2D(Collider2D other)
{
if(other.name=="EdgeBox")
{
Destroy(gameObject);
}
}
为了看到效果,我们可以把Main Camera的Size设置为7。
这么说来,主角也可能跑到屏幕外去,顺便也做个挡住玩家的空气墙吧
再次新建一个Empty GameObject叫做BoundaryBox,很多设置都与上面类似,这里我就贴一部分吧。另外如果只是墙的作用的话,就不用额外添加脚本了,Unity自带刚体间的互斥作用。
计算敌人1的飞行角度
要让敌人1的飞入边和飞出边是对边,这样才有足够的飞行时间
之前我们的思路是,随机生成飞入点,然后再随机相应飞行角度。现在既然要限定飞出点的范围,那么就改为随机飞入点和飞出点,两点也能确定一条直线。
另外这里也做一个小优化,现在点的坐标都在10x10的周长上,但这样其实是不自然的,Enemy1是不完全在屏幕外生成的,导致有种突然出现的感觉(虽然不明显)。因此我们稍微拉远一点,在10.5x10.5的边缘生成。
对enemy1MoveController
修改:
public float moveSpeed;
private float xPos;
private float yPos;
private float wPos;
private float vPos;
// Use this for initialization
void Start()
{
//{0,1,2,3}={右边缘,上边缘,左边缘,下边缘}
int direction = Random.Range(0, 4);
switch (direction)
{
case 0:
xPos = 5.5f;
yPos = Random.Range(-5.0f, 5.0f);
wPos = -5.5f;
vPos = Random.Range(-5.0f, 5.0f);
break;
case 1:
xPos = Random.Range(-5.0f, 5.0f);
yPos = 5.5f;
wPos = Random.Range(-5.0f, 5.0f);
vPos = -5.5f;
break;
case 2:
xPos = -5.5f;
yPos = Random.Range(-5.0f, 5.0f);
wPos = 5.5f;
vPos = Random.Range(-5.0f, 5.0f);
break;
case 3:
xPos = Random.Range(-5.0f, 5.0f);
yPos = -5.5f;
wPos = Random.Range(-5.0f, 5.0f);
vPos = 5.5f;
break;
}
//位置初始化
transform.position = new Vector2(xPos, yPos);
}
// Update is called once per frame
void Update()
{
Vector2 dir = new Vector2(wPos - xPos, vPos - yPos).normalized;
transform.Translate(dir * moveSpeed * Time.deltaTime);
}
代码说明:
1.vector2.normalized是将Vector2的一个实例转换为同方向的单位向量,我这么做的目的就是让向量单纯表示方向,moveSpeed就能统一所有Enemy1的速度。
2.飞入点为(x,y),飞出点为(w,v)。
优化敌人生成器
现在这游戏还不能玩的原因是敌人太多了,我们要让生成间隔减小一点。
目前Enemy1是1帧生成一个,不如把这个1变为一个公开变量,这样可以自己不断调试来确定一个合理的值。
对enemy1BornController
修改:
public class enemy1BornController : MonoBehaviour {
public GameObject pre_Enemy1;
public float bornColdDown;//Enemy1的生成间隔
private float pasttimeSingle;//单个Enemy1生成后已过去的时间
// Use this for initialization
void Start ()
{
}
// Update is called once per frame
void Update () {
pasttimeSingle += Time.deltaTime;
//达到冷却时间
if(pasttimeSingle>= bornColdDown)
{
pasttimeSingle -= bornColdDown;
Instantiate(pre_Enemy1);
}
}
}
代码说明:
1.相当于每一帧都累加一次时间数,达到所需的冷却时间就生成一个Enemy1并刷新时间数。
回到Unity里,这个脚本是在Main Camera上面的,这时候就多了一个Born Cold Down供我们填写,我们填入0.25,也就是1秒4个的生成速度。
这样,一个非常非常非常简陋的小游戏就算“小”功告成了!