前言
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文章来源:https://www.toymoban.com/news/detail-717270.html
找到exe文件运行,就可以直接游玩游戏了。文章来源地址https://www.toymoban.com/news/detail-717270.html
到了这里,关于unity入门项目Ruby‘s Adventure的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!