Bootstrap

[Unity]《太空射击》开发日记Ep.2(初级篇)

前言

[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文件夹,再在里面自定义我们自己的文件夹。新的文件目录就长这样
在这里插入图片描述
剩下的就很简单,直接把PlayerEnemy1拖到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之后,应该要受到伤害
这个效果的核心在于检测"碰撞",这需要我们给PlayerEnemy1定义有效碰撞区域。当两个区域重叠后,就视为发生碰撞。
第一步,给两个预制件添加碰撞区域。
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个的生成速度。
在这里插入图片描述
这样,一个非常非常非常简陋的小游戏就算“小”功告成了!

;