unity入门项目Ruby‘s Adventure

前言

Ruby's Adventure是unity官方提供的一个案例,当初入门就是做的这个项目,学到了很多unity的基本操作。这篇文章记录了这个游戏从创建项目到发布的过程。

游戏简介:城镇里的机器人都因为缺少零件而失控,我们的主角狐狸Ruby收到青蛙先生的委托后,拿上了齿轮,去修复失控的机器人。

一、游戏效果

玩家可以用W、A、S、D控制Ruby移动,Ruby在触碰到失控的机器人和陷阱后生命值会降低,在和青蛙先生对话后可以按H发射齿轮,齿轮命中机器人后机器人会被修好。

二、前期准备

1.创建项目

用Unity Hub创建一个2d项目

2.资源导入

在Unity官方的AssetStore可以免费获取资源

获取资源后可以在Unity的Package Manager中导入资源

三、人物创建

3.1创建人物和脚本

拖入主角Ruby的图片素材,拖入的物体叫精灵(sprite)

新建一个脚本RubyController并挂载在人物上,然后就可以写脚本来控制Ruby了。

在visula studio中可以看到,Unity会自动生成RubyController类,并有Start()和Update()两个函数,Start()会在项目的第一帧调用一次,Update()会在之后的每一帧调用一次

3.2实现主角的移动

获取键盘输入,并用rigidbody2d移动主角,这样在按下W、A、S、D后Ruby就会移动了

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RubyController : MonoBehaviour
{
    private Rigidbody2D rigidbody2d;
    // Start is called before the first frame update
    void Start()
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
    }

    // Update is called once per frame
    void Update()
    {
        //获取键盘的轴向输入
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        //创建一个position记录移动后的位置
        Vector2 position = transform.position;
        //改变x,y轴上位置
        position.x = position.x + 3 * horizontal * Time.deltaTime;
        position.y = position.y + 3 * vertical * Time.deltaTime;
        //将主角的位置置为移动后的位置
        rigidbody2d.MovePosition(position);
    }
}

四、场景布局

4.1背景布置

为了能够快速布置游戏背景,可以用Tilemap工具来构建瓦片地图,创建瓦片地图时,Grid组件自动作为瓦片地图的父级,相比于传统使用图片搭建地图的方式,使用瓦片地图用来搭建地图更快。

添加后发现场景上出现了很多格子,可以使用Tile Palette里的Tile在上面绘制,Tilemap可以理解为画布,Tile Palette可以理解为调色板,Tile就是调色板上的颜料。

现在Tilemap中还没有Tile,先将资源切割,添加到Tile Palette中

然后用Tile Palette就可以方便的绘制2D场景了

4.2设置渲染层级

我们看到背景覆盖了我们的主角Ruby,导致我们看不到我们的Ruby了,因为它们的Order in Layer都是0

为了让Ruby在背景之上,将Tilemap的Order in Layer设为-10,因为背景一定在最下面

4.3丰富游戏场景

游戏只有背景显然是不够的,拖入更多装饰物,使场景更加丰富

4.4控制渲染顺序

如果Ruby站在一个物体的前面,Ruby应该会挡住它,如果站在后面,则会被挡住

为了实现这个功能,可以根据y轴控制渲染顺序,在Project Settiing中改变Graphics的参数

把sprite sort point设置为pivot(默认是中心),并将pivot放在一个合适的位置

以盒子为例,更改pivot到最下面,因为在那个点之下的物体会在盒子之前显示,之后的会被盒子挡住

同时设置Ruby的pivot,实现合理的显示顺序

之后把之前添加的物体都进行上述操作

五、物理系统

5.1精灵的物理系统

Ruby一直朝一个物体走时,应该要被挡住,而不是穿过去,为了实现这个效果,要为物体添加刚体和碰撞器

碰撞检测的其中一方必须要有刚体(最好是动的一方)

添加刚体,并将重力设为0,防止物体下坠,并冻结z轴的旋转,防止碰撞时Ruby旋转。

碰撞检测的双方都要有碰撞器,给盒子和Ruby都添加加碰撞器,并修改到合适的大小,这样Ruby碰到盒子就会被挡住了,之后把其他物体也进这样的操作

5.2瓦片的物理系统

单独的瓦片没有办法添加碰撞器

所以给Tilemap添加Tilemap Collider 2d和Composite Collider 2D

并给需要碰撞器的瓦片(比如池塘)的Collider Type设为gird

六、添加生命值、道具

6.1添加生命值

Ruby是有生命值的,在触碰陷阱、敌人和草莓时生命值会改变,生命值小于等于0时会重生,所以需要添加相关代码。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RubyController : MonoBehaviour
{
    private Rigidbody2D rigidbody2d;
    //Ruby的移动速度
    public int speed = 5;
    //最大生命值
    public int maxHealth = 5;
    //Ruby的当前生命值
    private int currentHealth;
    //重生地点
    private Vector3 respawnPosition;
    //Ruby的无敌时间
    public float timeInvincible = 2.0f; //无敌时间常量
    private bool isInvincible;
    //计时器
    public float invincibleTimer;

    //外部可以访问Ruby的血量
    public int Health { get { return currentHealth; } }
    void Start()
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
        //游戏开始时满血
        currentHealth = maxHealth;
        //重生地点为出生地
        respawnPosition = transform.position;
        //开始时无敌
        isInvincible = true;
    }

    // Update is called once per frame
    void Update()
    {
        //获取键盘的轴向输入
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        //创建一个position记录移动后的位置
        Vector2 position = transform.position;
        //改变x,y轴上位置
        position.x = position.x + speed * horizontal * Time.deltaTime;
        position.y = position.y + speed * vertical * Time.deltaTime;
        //将主角的位置置为移动后的位置
        rigidbody2d.MovePosition(position);

        if (isInvincible)
        {
            invincibleTimer = invincibleTimer - Time.deltaTime;
            if (invincibleTimer <= 0)
            {
                //无敌时间结束,Ruby又可以受到伤害了
                isInvincible = false;
            }
        }
    }
    //改变血量的函数
    public void ChangeHealth(int amount)
    {
        if (amount < 0)
        {
            //如果是无敌状态,直接return
            if (isInvincible)
            {
                return;
            }
            //受到伤害
            isInvincible = true;
            invincibleTimer = timeInvincible;
        }
        currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth); //将血量currentHealth限定在0到maxHealth
        //Debug.Log("Ruby当前的生命值是:"+currentHealth + "/" + maxHealth);
        
        //血量小于等于0会重生
        if (currentHealth <= 0)
        {
            Respawn();
        }
    }
    //重生方法
    private void Respawn()
    {
        ChangeHealth(maxHealth);
     	//改变Ruby位置
        transform.position = respawnPosition;
    }
}

6.2制作血量回复道具

制作生命回复道具草莓,并添加脚本,以实现Ruby不是满血时回复一点生命值。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HealthCollectible : MonoBehaviour
{
    // Start is called before the first frame update
    private void OnTriggerEnter2D(Collider2D collision)
    {
        //Debug.Log("与我们发生碰撞的是:" + collision);
        RubyController rubyController = collision.GetComponent<RubyController>();
        //接触到的物体有RubyController组件,即接触到的物体是Ruby
        if (rubyController != null)
        {
            if (rubyController.Health < rubyController.maxHealth)//Ruby不满血
            {
                //回复一点生命值
                rubyController.ChangeHealth(1);
                //将草莓销毁
                Destroy(gameObject);
            }
        }
    }
}

制作陷阱区域DamageZone,Ruby触碰到后生命值减1,并有一段无敌时间,防止一直扣血,无敌时间过后如果还在区域内,继续扣一点生命值。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DamageZone : MonoBehaviour
{
    // Start is called before the first frame update
    private void OnTriggerStay2D(Collider2D collision)
    {
        RubyController rubyController = collision.GetComponent<RubyController>();
        if (rubyController != null)//接触到的物体有RubyController组件,即接触到的物体是Ruby
        {
            rubyController.ChangeHealth(-1);
        }
    }
}

七、添加敌人

添加一个敌人,并添加好刚体、碰撞器和脚本,编写脚本,使敌人自动来回移动,当敌人和Ruby相接触时,Ruby的生命值减少。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyController : MonoBehaviour
{
    public float speed = 3;
    private Rigidbody2D rigidbody2d;

    public bool vertical;
    //方向控制
    private int direction = 1;
    //改变移动方向的时间
    public float changeTime = 3;
    private float timer;

    void Start()
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
    }
    // Update is called once per frame
    void Update()
    {
        //以下都是移动
        timer -= Time.deltaTime;
        //时间减到比0小时,改变移动方向
        if (timer < 0)
        {
            direction = -direction;
            //animator.SetFloat("MoveX", direction);
            timer = changeTime;
        }
        Vector2 position = rigidbody2d.position;
        if (vertical)
        {
            position.y = position.y + Time.deltaTime * speed * direction;
        }
        else
        {
            position.x = position.x + Time.deltaTime * speed * direction;
        }
        rigidbody2d.MovePosition(position);
    }
    //和Ruby接触时,Ruby生命值降低
    private void OnCollisionEnter2D(Collision2D collision)
    {
        RubyController rubyController = collision.gameObject.GetComponent<RubyController>();
        //判断接触到的是Ruby
        if (rubyController != null)
        {
            rubyController.ChangeHealth(-1);
        }
    }
}

八、动画系统

到现在为止,Ruby和敌人移动时只是一张图片在平移,为了播放对应的动画,需要使用Unity的动画系统。

8.1敌人的动画

创建一个动画控制器,添加到敌人上。

创建好上下左右移动的动画

制作好四个方向的动画之后,拖到动画控制器中,使用混合树来控制动画切换。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyController : MonoBehaviour
{
    public float speed = 3;
    private Rigidbody2D rigidbody2d;

    private Animator animator;

    public bool vertical;
    //方向控制
    private int direction = 1;
    public float changeTime = 3;
    private float timer;


    void Start()
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {

        //以下都是移动
        timer -= Time.deltaTime;
        if (timer < 0)
        {
            //改变移动方向
            direction = -direction;
            PlayMoveAnimation();
            //animator.SetFloat("MoveX", direction);
            timer = changeTime;
        }
        Vector2 position = rigidbody2d.position;
        if (vertical)
        {
            position.y = position.y + Time.deltaTime * speed * direction;
        }
        else
        {
            position.x = position.x + Time.deltaTime * speed * direction;
        }
        rigidbody2d.MovePosition(position);
    }
    //和Ruby接触时,Ruby生命值降低
    private void OnCollisionEnter2D(Collision2D collision)
    {
        RubyController rubyController = collision.gameObject.GetComponent<RubyController>();
        //判断接触到的是Ruby
        if (rubyController != null)
        {
            rubyController.ChangeHealth(-1);
        }
    }

    //控制移动动画的方法
    private void PlayMoveAnimation()
    {
        //改变参数,已实现动画的方向的变化
        if (vertical)
        {
            animator.SetFloat("MoveX", 0);
            animator.SetFloat("MoveY", direction);
        }
        else
        {
            animator.SetFloat("MoveX", direction);
            animator.SetFloat("MoveY", 0);
        }
    }
}

8.2Ruby的动画

用同样的方法制作Ruby的动画,官方的资源中已经制作好了Ruby的控制器,动画和参数都已经设置好了

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RubyController : MonoBehaviour
{
    private Rigidbody2D rigidbody2d;
    //Ruby的移动速度
    public int speed = 5;
    //最大生命值
    public int maxHealth = 5;
    //Ruby的当前生命值
    private int currentHealth;
    //重生地点
    private Vector3 respawnPosition;

    //Ruby的无敌时间
    public float timeInvincible = 2.0f; //无敌时间常量
    private bool isInvincible;
    //计时器
    public float invincibleTimer;

    //外部可以访问Ruby的血量
    public int Health { get { return currentHealth; } }

    private Animator animator;
    //创建向量,表示方向
    private Vector2 lookDirection = new Vector2(1, 0);

    public GameObject projectilePrefab;
    void Start()
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
        //游戏开始时满血
        currentHealth = maxHealth;
        //重生地点为出生地
        respawnPosition = transform.position;
        //开始时无敌
        isInvincible = true;
        //获取动画控制器
        animator = GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        //获取键盘的轴向输入
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        Vector2 move = new Vector2(horizontal, vertical);
        if (!Mathf.Approximately(move.x, 0) || !Mathf.Approximately(move.y, 0))
        {
            lookDirection.Set(move.x, move.y); //lookDirection=move;
            lookDirection.Normalize();

        }
		//控制动画的方向
        animator.SetFloat("Look X", lookDirection.x);
        animator.SetFloat("Look Y", lookDirection.y);
        animator.SetFloat("Speed", move.magnitude);//move的模长


        //创建一个position记录移动后的位置
        Vector2 position = transform.position;
        //改变x,y轴上位置
        //position.x = position.x + speed * horizontal * Time.deltaTime;
        //position.y = position.y + speed * vertical * Time.deltaTime;
        position = position + speed * move * Time.deltaTime;
        //将主角的位置置为移动后的位置
        rigidbody2d.MovePosition(position);

        if (isInvincible)
        {
            invincibleTimer = invincibleTimer - Time.deltaTime;
            if (invincibleTimer <= 0)
            {
                isInvincible = false;
            }
        }
    }
    //改变血量的函数
    public void ChangeHealth(int amount)
    {
        if (amount < 0)
        {
            //如果是无敌状态,直接return
            if (isInvincible)
            {
                return;
            }
            //受到伤害
            isInvincible = true;
            invincibleTimer = timeInvincible;
        }
        currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth); //将血量currentHealth限定在0到maxHealth
                                                                           //Debug.Log("Ruby当前的生命值是:"+currentHealth + "/" + maxHealth);

        //血量小于等于0重生
        if (currentHealth <= 0)
        {
            Respawn();
        }
    }

    private void Respawn()
    {
        ChangeHealth(maxHealth);
        transform.position = respawnPosition;
    }
}

九、发射齿轮

Ruby可以发射齿轮,击中物体会消失,如果击中的是敌人,敌人会被修好。另外,如果没有击中物体,也要让齿轮飞行一段距离后消失,否则齿轮会越来越多,消耗资源

9.1齿轮的开发

为了能够发射很多子弹,可以让Ruby发射齿轮时在Ruby面前生成一个齿轮,给它一个向前的力,如果碰到物体或飞行了一定的时间后就消失。

。首先创建一个子弹,添加刚体、碰撞器和脚本,编写脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Projectile : MonoBehaviour
{
    // Start is called before the first frame update
    private Rigidbody2D rigidbody2d;
    void Awake() //在实例化之前运行 用start会报错 原因:在主角中实例化后获取脚本,直接跑方法,而不运行start
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
    }
    private void Update()
    {
        if (transform.position.magnitude > 20)
        {
            Destroy(gameObject);
        }
    }
    public void Launch(Vector2 direction, float force)
    {
        rigidbody2d.AddForce(direction * force);
    }
    private void OnCollisionEnter2D(Collision2D collision)
    {
        //Debug.Log("当前触碰到的物体是:" + collision.gameObject);
        Destroy(gameObject);
    }
}

RubyController也要修改,玩家按H时发射齿轮,并播放Hit动画。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RubyController : MonoBehaviour
{
    private Rigidbody2D rigidbody2d;
    //Ruby的移动速度
    public int speed = 5;
    //最大生命值
    public int maxHealth = 5;
    //Ruby的当前生命值
    private int currentHealth;
    //重生地点
    private Vector3 respawnPosition;

    //Ruby的无敌时间
    public float timeInvincible = 2.0f; //无敌时间常量
    private bool isInvincible;
    //计时器
    public float invincibleTimer;

    //外部可以访问Ruby的血量
    public int Health { get { return currentHealth; } }

    private Animator animator;
    //创建向量,表示方向
    private Vector2 lookDirection = new Vector2(1, 0);

    public GameObject projectilePrefab;
    void Start()
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
        //游戏开始时满血
        currentHealth = maxHealth;
        //重生地点为出生地
        respawnPosition = transform.position;
        //开始时无敌
        isInvincible = true;
        //获取动画控制器
        animator = GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        //获取键盘的轴向输入
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        Vector2 move = new Vector2(horizontal, vertical);
        if (!Mathf.Approximately(move.x, 0) || !Mathf.Approximately(move.y, 0))
        {
            lookDirection.Set(move.x, move.y); //lookDirection=move;
            lookDirection.Normalize();

        }

        animator.SetFloat("Look X", lookDirection.x);
        animator.SetFloat("Look Y", lookDirection.y);
        animator.SetFloat("Speed", move.magnitude);//move的模长


        //创建一个position记录移动后的位置
        Vector2 position = transform.position;
        //改变x,y轴上位置
        //position.x = position.x + speed * horizontal * Time.deltaTime;
        //position.y = position.y + speed * vertical * Time.deltaTime;
        position = position + speed * move * Time.deltaTime;
        //将主角的位置置为移动后的位置
        rigidbody2d.MovePosition(position);

        if (isInvincible)
        {
            invincibleTimer = invincibleTimer - Time.deltaTime;
            if (invincibleTimer <= 0)
            {
                isInvincible = false;
            }
        }
        //按H发射齿轮
        if (Input.GetKeyDown(KeyCode.H))
        {
            Launch();
        }
    }
    //改变血量的函数
    public void ChangeHealth(int amount)
    {
        if (amount < 0)
        {
            //如果是无敌状态,直接return
            if (isInvincible)
            {
                return;
            }
            //受到伤害
            isInvincible = true;
            invincibleTimer = timeInvincible;
        }
        currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth); //将血量currentHealth限定在0到maxHealth
                                                                           //Debug.Log("Ruby当前的生命值是:"+currentHealth + "/" + maxHealth);

        if (currentHealth <= 0)
        {
            Respawn();
        }
    }

    private void Launch()
    {

        GameObject projectileObject = Instantiate(projectilePrefab, rigidbody2d.position+Vector2.up*0.5f, Quaternion.identity);
        Projectile projectile= projectileObject.GetComponent<Projectile>();
        projectile.Launch(lookDirection,300);
        animator.SetTrigger("Launch");
    }

    private void Respawn()
    {
        ChangeHealth(maxHealth);
        transform.position = respawnPosition;
    }
}

当齿轮创建出来时,检测到Ruby,所以会立刻消失,为了让齿轮和Ruby不发生碰撞建检测,需要把他们放在两个不同的层级,并让这两个层级不发生碰撞检测

这样就可以正常发射齿轮了

9.2敌人被击中的效果

敌人被击中时,会被修复,不再移动和造成伤害

需要修改敌人和齿轮的脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyController : MonoBehaviour
{
    public float speed = 3;
    private Rigidbody2D rigidbody2d;

    private Animator animator;

    public bool vertical;
    //方向控制
    private int direction = 1;
    public float changeTime = 3;
    private float timer;

    //判断当前机器人是否故障
    private bool broken;

    void Start()
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
        //开始时机器人是损坏的
        broken = true;
    }

    // Update is called once per frame
    void Update()
    {
        if (!broken)
        {
            //如果修好,不移动
            return;
        }
        //以下都是移动
        timer -= Time.deltaTime;
        if (timer < 0)
        {
            //改变移动方向
            direction = -direction;
            PlayMoveAnimation();
            //animator.SetFloat("MoveX", direction);
            timer = changeTime;
        }
        Vector2 position = rigidbody2d.position;
        if (vertical)
        {
            position.y = position.y + Time.deltaTime * speed * direction;
        }
        else
        {
            position.x = position.x + Time.deltaTime * speed * direction;
        }
        rigidbody2d.MovePosition(position);
    }
    private void OnCollisionEnter2D(Collision2D collision)
    {
        RubyController rubyController = collision.gameObject.GetComponent<RubyController>();
        if (rubyController != null)
        {
            rubyController.ChangeHealth(-1);
        }
    }

    //控制移动动画的方法
    private void PlayMoveAnimation()
    {
        if (vertical)
        {
            animator.SetFloat("MoveX", 0);
            animator.SetFloat("MoveY", direction);
        }
        else
        {
            animator.SetFloat("MoveX", direction);
            animator.SetFloat("MoveY", 0);
        }
    }
    //修复方法
    public void Fix()
    {
        broken = false;
        rigidbody2d.simulated = false;
        //播放修复好的动画
        animator.SetTrigger("Fixed");
    }
}

齿轮在检测到碰撞到的物体是敌人后,会调用敌人的Fix()方法,实现机器人的修复

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Projectile : MonoBehaviour
{
    // Start is called before the first frame update
    private Rigidbody2D rigidbody2d;
    void Awake() //在实例化之前运行 用start会报错 原因:在主角中实例化后获取脚本,直接跑方法,而不运行start
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
    }
    private void Update()
    {
        if (transform.position.magnitude > 20)
        {
            Destroy(gameObject);
        }
    }
    public void Launch(Vector2 direction, float force)
    {
        rigidbody2d.AddForce(direction * force);
    }
    private void OnCollisionEnter2D(Collision2D collision)
    {
        //Debug.Log("当前触碰到的物体是:" + collision.gameObject);
        EnemyController enemyController = collision.gameObject.GetComponent<EnemyController>();
        if (enemyController != null)
        {
            enemyController.Fix();
        }
        Destroy(gameObject);
    }
}

十.镜头跟随

为了让Ruby移动时镜头能够跟随Ruby,需要添加虚拟相相机

10.1添加虚拟相机

在Package Manager中导入包

创建一个虚拟相机

让虚拟相机跟随Ruby,并添加限制器,防止视角跑出地图外

十一.完善地图并制作地图边界

添加更多物体,使地图更加丰富。并制作好边界,使相机的范围不会超过限制范围

十二、粒子系统

12.1烟雾特效

为了让机器人损坏时有冒烟的特效,添加一个粒子系统

并调整参数和图片,已实现烟雾效果

修改机器人的脚本,时机器人被修复时不播放烟雾效果

//烟雾系统
public ParticleSystem smokeEffect;

//....重复代码省略

    public void Fix()
    {
        broken = false;
        rigidbody2d.simulated = false;
        animator.SetTrigger("Fixed");
        //烟雾停止
        smokeEffect.Stop();
    }

12.2击打特效

制作击打特效,在齿轮碰撞物体时产生

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Projectile : MonoBehaviour
{
    // Start is called before the first frame update
    private Rigidbody2D rigidbody2d;
    //爆炸特效
    public GameObject effectParticle;
    void Awake() //在实例化之前运行 用start会报错 原因:在主角中实例化后获取脚本,直接跑方法,而不运行start
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
    }
    private void Update()
    {
        if (transform.position.magnitude > 20)
        {
            Destroy(gameObject);
        }
    }
    public void Launch(Vector2 direction, float force)
    {
        rigidbody2d.AddForce(direction * force);
    }
    private void OnCollisionEnter2D(Collision2D collision)
    {
        //Debug.Log("当前触碰到的物体是:" + collision.gameObject);
        //原地生成爆炸特效
        EnemyController enemyController = collision.gameObject.GetComponent<EnemyController>();
        if (enemyController != null)
        {
            enemyController.Fix();
        }
        Destroy(gameObject);
    }
}

12.3回血特效

Ruby拾取草莓时生成回血特效

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HealthCollectible : MonoBehaviour
{
    //回复特效
    public GameObject effectParticle;
    // Start is called before the first frame update
    private void OnTriggerEnter2D(Collider2D collision)
    {
        //Debug.Log("与我们发生碰撞的是:" + collision);
        RubyController rubyController = collision.GetComponent<RubyController>();
        //接触到的物体有RubyController组件,即接触到的物体是Ruby
        if (rubyController != null)
        {
            if (rubyController.Health < rubyController.maxHealth)//Ruby不满血
            {
                //原地生成回复特效
                Instantiate(effectParticle, transform.position, Quaternion.identity);
                rubyController.ChangeHealth(1);
                Destroy(gameObject);
            }
        }

    }
}

十三.UGUI

13.1血条制作

为了显示Ruby的血量,创建一个Canvas,在里面制作出血条,放在屏幕左上角

用血量图片的伸缩表示血量的多少,需要用脚本控制血条变化

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UIHealthBar : MonoBehaviour
{
    public Image mask;
    float originalSize;
    public static UIHealthBar instance { get; private set; }
    public int fixedNum=0;
    // Start is called before the first frame update
    public void Awake()
    {
        instance = this;
    }
    void Start()
    {
        originalSize = mask.rectTransform.rect.width;
    }
    // Update is called once per frame
    void Update()
    {
        
    }
    public void SetValue(float fillPercent)
    {
        mask.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, originalSize * fillPercent);
    }
}

血条会随血量变化而变化

十四、制作NPC

游戏中需要NPC,Ruby和NPC对话后可以发射子弹,并且当Ruby修好所有机器人后NPC和Ruby的对话会改变

14.1NPC创建

拖入NPC的图片,并添加动画控制器,制作动画,添加碰撞器

14.2与NPC对话

在NPC面前创建对话框

当Ruby在NPC面前按下T时,对话框出现

先创建NPC的脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class NPCDialog : MonoBehaviour
{
    public GameObject dialogBox;
    public float displayTime=3.0f;
    private float timerDisplay=-1;
    public Text dialogText;
    public AudioSource audioSource;
    public AudioClip winSound;
    public bool hasPlayed=false;
    // Start is called before the first frame update
    void Start()
    {
        timerDisplay = displayTime;
        dialogBox.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        if (timerDisplay >= 0)
        {
            timerDisplay = timerDisplay - Time.deltaTime;
        }
        else
        {
            dialogBox.SetActive(false);
        }
    }
    public void DisPlayDialog()
    {
        timerDisplay = displayTime;
        dialogBox.SetActive(true);
    }
}

为了检测Ruby是否在NPC面前,使用射线检测,检测到NPC时,显示对话框

在RubyController中的Update中添加if语句,如果检测到的物体是在NPC的layer中,调用NPC的脚本显示对话框,因此需要把NPC放在“NPC”的layer中

    if(Input.GetKeyDown(KeyCode.T))
    {
		//射线检测到NPC
        RaycastHit2D hit = Physics2D.Raycast(rigidbody2d.position + 				   Vector2.up*0.2f,lookDirection,1.5f,LayerMask.GetMask("NPC"));
        if(hit.collider!=null)
        {
            NPCDialog npcDialog = hit.collider.GetComponent<NPCDialog>();
            if(npcDialog!=null)
            {
                npcDialog.DisPlayDialog();
            }
        }
    }

十五、添加音乐和音效

15.1背景音乐

添加一个空物体,添加Audio Source并添加背景音乐,勾选loop循环播放

15.2添加音效

给Ruby挂载上音效,实现受伤、发射、走路的音效

using System.Collections;
using System.Collections.Generic;
using UnityEngine; 

public class RubyController : MonoBehaviour
{
    private Rigidbody2D rigidbody2d;
    public int speed = 10;//Ruby的速度
    public int maxHealth = 5;//最大生命值
    private int currentHealth;//Ruby的当前生命值

    //Ruby的无敌时间
    public float timeInvincible = 2.0f; //无敌时间常量
    private bool isInvincible;
    public float invincibleTimer;//计时器

    private Vector2 lookDirection = new Vector2(1, 0);
    private Animator animator;

    public GameObject projectilePrefab;

    //音频资源
    public AudioSource audioSource;
    public AudioSource walkAudioSource;
    public AudioClip playerHit;
    public AudioClip attackSoundClip;
    public AudioClip walkSound;
    public int Health { get { return currentHealth; }} //get只读 set{health=value;} 写

    private Vector3 respawnPosition;

    // Start is called before the first frame update
    void Start()
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
        currentHealth = maxHealth;

        animator = GetComponent<Animator>();
        isInvincible = true;
        //int a = GetRubyHealthValue();
        //Debug.Log("Ruby当前的血量是:" + a);
        //SpriteRenderer dog = GetComponent<SpriteRenderer>();

        audioSource = GetComponent<AudioSource>();
        respawnPosition = transform.position;
    }

    // Update is called once per frame
    void Update()
    {
        float h=Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");

        Vector2 move = new Vector2(h, v);
        if (!Mathf.Approximately(move.x, 0) || !Mathf.Approximately(move.y, 0))
        {
            lookDirection.Set(move.x, move.y); //lookDirection=move;
            lookDirection.Normalize();
            //如果走路音效没有在播放,播放走路音效
            if(!walkAudioSource.isPlaying)
            {
                walkAudioSource.Play();
                walkAudioSource.clip = walkSound;
            }
        }
        else
        {
            //已经在播放则不播放
            walkAudioSource.Stop();
        }
        //控制动画方向
        animator.SetFloat("Look X",lookDirection.x);
        animator.SetFloat("Look Y",lookDirection.y);
        animator.SetFloat("Speed", move.magnitude);//move的模长

        Vector2 position = transform.position;
        //position.x = position.x + speed*h*Time.deltaTime;
        //position.y = position.y + speed*v*Time.deltaTime;
        position = position + speed * move * Time.deltaTime;
        rigidbody2d.MovePosition(position);

        if(isInvincible)
        {
            invincibleTimer = invincibleTimer - Time.deltaTime;
            if(invincibleTimer<=0)
            {
                isInvincible = false;
            }
        }
        if(Input.GetKeyDown(KeyCode.H))
        {
            Launch();
        }
        if(Input.GetKeyDown(KeyCode.T))
        {

            RaycastHit2D hit = Physics2D.Raycast(rigidbody2d.position + Vector2.up*0.2f,lookDirection,1.5f,LayerMask.GetMask("NPC"));
            if(hit.collider!=null)
            {
                NPCDialog npcDialog = hit.collider.GetComponent<NPCDialog>();
                if(npcDialog!=null)
                {
                    npcDialog.DisPlayDialog();
                }
            }
        }

    }
    public void ChangeHealth(int amount)
    {
        if(amount<0)
        {
            if(isInvincible)
            {
                return;
            }
            animator.SetTrigger("Hit");
            isInvincible = true;
            //播放受伤音效
            PlaySound(playerHit);
            invincibleTimer = timeInvincible;
        }
        currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth); //将血量currentHealth限定在0到maxHealth
        //Debug.Log("Ruby当前的生命值是:"+currentHealth + "/" + maxHealth);
        UIHealthBar.instance.SetValue(currentHealth / (float)maxHealth);
        if(currentHealth<=0)
        {
                Respawn();
        }
        
    }
    private void Launch()
    {
        GameObject projectileObject = Instantiate(projectilePrefab, rigidbody2d.position+Vector2.up*0.5f, Quaternion.identity);
        Projectile projectile= projectileObject.GetComponent<Projectile>();
        projectile.Launch(lookDirection,300);
        animator.SetTrigger("Launch");
        //发射时播放发射音频
        PlaySound(attackSoundClip);
      
    }

    //播放音效的方法
    public void PlaySound(AudioClip audioClip)
    {
        audioSource.PlayOneShot(audioClip);
    }

    private void Respawn()
    {
        ChangeHealth(maxHealth);
        transform.position = respawnPosition;
    }
}

Ruby拾取草莓回血后,也会播放音效

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class HealthCollectible : MonoBehaviour
{
    public AudioClip audioClip;
    //回复特效
    public GameObject effectParticle;
    // Start is called before the first frame update
    private void OnTriggerEnter2D(Collider2D collision)
    {
        //Debug.Log("与我们发生碰撞的是:" + collision);
        RubyController rubyController = collision.GetComponent<RubyController>();
        if(rubyController!=null)//接触到的物体有RubyController组件,即接触到的物体是Ruby
        {
            if (rubyController.Health<rubyController.maxHealth)//Ruby不满血
            {
                //原地生成一个回复特效
                Instantiate(effectParticle, transform.position, Quaternion.identity);
                rubyController.ChangeHealth(1);
                //播放回血音效
                rubyController.PlaySound(audioClip);
                Destroy(gameObject);
            }
        }
    }
}

机器人走动时、被修好时也有音效

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyController : MonoBehaviour
{
    public float speed=3;
    private Rigidbody2D rigidbody2d;

    public bool vertical;
    //方向控制
    private int direction = 1;
    public float changeTime = 3;
    private float timer;

    private Animator animator;
    //当前机器人是否故障
    private bool broken;

    public ParticleSystem smokeEffect;
    // Start is called before the first frame update

    //音频资源
    private AudioSource audioSource;
    public AudioClip fixSound;
    public AudioClip[] hitSounds;

    public GameObject hitEffectParticle;
    void Start()
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
        timer = changeTime;
        animator = GetComponent<Animator>();
        //animator.SetFloat("MoveX", direction);
        //animator.SetBool("Vertical", vertical);
        PlayMoveAnimation();
        broken = true;
        audioSource = GetComponent<AudioSource>();
    }

    // Update is called once per frame
    void Update()
    {
        if(!broken)
        {
            //如果修好,不移动
            return;
        }
        //以下都是移动
        timer -= Time.deltaTime;
        if(timer<0)
        {
            direction = -direction;
            //animator.SetFloat("MoveX", direction);
            PlayMoveAnimation();

            timer = changeTime;
        }
        Vector2 position = rigidbody2d.position;
        if(vertical)
        {
            position.y = position.y + Time.deltaTime * speed*direction;
        }
        else
        {
            position.x = position.x + Time.deltaTime * speed*direction;
        }
        rigidbody2d.MovePosition(position);
    }
    private void OnCollisionEnter2D(Collision2D collision)
    {
        RubyController rubyController = collision.gameObject.GetComponent<RubyController>();
        if(rubyController!=null)
        {
            rubyController.ChangeHealth(-1);
        }
    }

    //控制移动动画的方法
    private void PlayMoveAnimation()
    {
        if (vertical)
        {
            animator.SetFloat("MoveX", 0);
            animator.SetFloat("MoveY", direction);
        }
        else
        {
            animator.SetFloat("MoveX", direction);
            animator.SetFloat("MoveY", 0);
        }
    }

    public void Fix()
    {
        Instantiate(hitEffectParticle, transform.position, Quaternion.identity);
        broken = false;
        rigidbody2d.simulated = false;
        animator.SetTrigger("Fixed");
        smokeEffect.Stop();

        int randomNum = Random.Range(0, 2);
        //被修好时停止走路音频
        audioSource.Stop();
        audioSource.volume = 0.5f;
        //播放击中音效
        audioSource.PlayOneShot(hitSounds[randomNum]);
        Invoke("PlayFixSound",0.1f);

    }

    //被修好音效的方法
    private void PlayFixSound()
    {
        audioSource.PlayOneShot(fixSound);
    }

}

十六、任务系统

游戏中Ruby并不是一开始就可以发射齿轮的,只有在接收了NPC的任务之后才能解锁发射齿轮。当Ruby修复好了所有机器人之后,任务完成,可以向NPC提交任务。

16.1接受任务

为了判断是都接受任务,需要一个全局变量hasTask,我们放在UIHealthBar中

//是否有任务
    public bool hasTask;

在NPCDialog中的DisPlayDialog()中添加语句,让Ruby在接受任务时将该值置为true

    public void DisPlayDialog()
    {
        timerDisplay = displayTime;
        dialogBox.SetActive(true);
        //接到任务
        UIHealthBar.instance.hasTask = true;
    }

修改Ruby的Lauch()方法

    private void Launch()
    {
		//没有任务直接return,无法发射齿轮
        if (UIHealthBar.instance.hasTask ==false)
        {
            return;
        }
        GameObject projectileObject = Instantiate(projectilePrefab, rigidbody2d.position+Vector2.up*0.5f, Quaternion.identity);
        Projectile projectile= projectileObject.GetComponent<Projectile>();
        projectile.Launch(lookDirection,300);
        animator.SetTrigger("Launch");
        PlaySound(attackSoundClip);
    }

这样只有在接收到任务时才能发射齿轮了。

16.2完成任务

为了判断任务是否完成,需要添加一个全局变量,fixedNum,每修好一个机器人,fixedNum就加一,当修好所有机器人后,任务就算完成了,对话框会修改,并且播放胜利音效

    //用修好的数量判断任务是否完成
    public int fixedNum=0;

敌人脚本的Fix()方法,当一个机器人执行Fix()方法后,就代表有一个机器人被修好,所以fixedNum加1

public void Fix()
{
    //修好的机器人数量加一
    UIHealthBar.instance.fixedNum++;
    Instantiate(hitEffectParticle, transform.position, Quaternion.identity);
    broken = false;
    rigidbody2d.simulated = false;
    animator.SetTrigger("Fixed");
    smokeEffect.Stop();

    int randomNum = Random.Range(0, 2);
    audioSource.Stop();
    audioSource.volume = 0.5f;
    audioSource.PlayOneShot(hitSounds[randomNum]);
    Invoke("PlayFixSound",0.1f);

}

NPCDialog的DisPlayDialog()方法

public void DisPlayDialog()
{
    timerDisplay = displayTime;
    dialogBox.SetActive(true);
    UIHealthBar.instance.hasTask = true;
    //所有机器人都被修好,我的场景中添加了5个机器人,所有fixedNum大鱼大于等于5时任务就完成了
    if(UIHealthBar.instance.fixedNum>=5)
    {
        //完成任务,修改对话框
        dialogText.text = "哦,谢谢你Ruby,你太棒了!";
        if(hasPlayed==false)
        {
            //播放胜利音效
            audioSource.PlayOneShot(winSound);
            hasPlayed = true;
        }  
    }
}

十七、游戏打包

游戏基本制作完成后,打包

设置好游戏图标等信息

然后在Build Settings中Build

找到exe文件运行,就可以直接游玩游戏了。

  • 12
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

捕赤鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值