射击(1/2)
我们的玩家飞船正在面临着一个非常可怕的Poulpi,但是我们不能够做任何事情...
让我们为飞船添加一个武器和一些弹药!这将会涉及到一些脚本代码,要有信心你能hold住。这些都是值得的。
弹药
首先在允许玩家射击之前我们需要定义一个游戏对象来表示它将使用的弹药。
这是精灵:
(右键保存图片)
弹药是一个我们经常使用的对象:当我们射击的时候在屏幕上会出现一些弹药的实例。
那么我们需要怎么做呢?当然是使用Prefab
准备prefab
你现在应该熟悉这个过程了:
- 导入纹理
- 在场景中创建一个新的Sprite
- 设置这个精灵的图像
- 添加一个“Rigidbody2D”并且将“Gravity scale“设置为0,然后“Fixed Angles”
- 添加一个大小是(1,1)的“Box Collider 2D"
设置缩放比为(0.75,0.75,1)来使它看起来正常
然而,这一次我们需要在“Inspector”设置一个新的参数:
- 在“Box Collider 2D”中检查“IsTrigger”属性
当发生碰撞的时候,碰撞触发器就会触发一个事件,但是碰撞触发器不是被物理模拟所使用。
它意味着当一个子弹碰到物体的时候是会穿过一个对象——这并不是真正意义上的交互。但是其他的碰撞将会有“OnTriggerEnter2D”事件发生。
这样,你有弹药了!是时候来编写它的行为脚本了。
创建一个新的脚本叫做“ShotScript”:
using UnityEngine;
/// <summary>
/// Projectile behavior
/// </summary>
public class ShotScript : MonoBehaviour
{
// 1 - Designer variables
/// <summary>
/// Damage inflicted
/// </summary>
public int damage = 1;
/// <summary>
/// Projectile damage player or enemies?
/// </summary>
public bool isEnemyShot = false;
void Start()
{
// 2 - Limited time to live to avoid any leak
Destroy(gameObject, 20); // 20sec
}
}
- 为速度,方向和伤害值变量初始化合适的值。
- 我们在20s之后摧毁这个对象。
将“ShotSprite”脚本绑定到精灵上。同时,也添加一个“MoveScript“脚本来使你的子弹移动。
之后拖拽“Project”面板上子弹游戏对象来创建一个prefab。在之后的的步骤中我们需要它。
你将会有如下配置:
点击“Play"按钮开始游戏之后,你将会看到这个子弹正在移动。
碰撞和伤害
然而,这个子弹还不能够摧毁任何的东西。
不要惊讶,我们还没有为伤害行为写脚本,我们仅仅需要一个新的脚本“HealthScript”:
using UnityEngine;
/// <summary>
/// Handle hitpoints and damages
/// </summary>
public class HealthScript : MonoBehaviour
{
/// <summary>
/// Total hitpoints
/// </summary>
public int hp = 1;
/// <summary>
/// Enemy or player?
/// </summary>
public bool isEnemy = true;
/// <summary>
/// Inflicts damage and check if the object should be destroyed
/// </summary>
/// <param name="damageCount"></param>
public void Damage(int damageCount)
{
hp -= damageCount;
if (hp <= 0)
{
// Dead!
Destroy(gameObject);
}
}
void OnTriggerEnter2D(Collider2D otherCollider)
{
// Is this a shot?
ShotScript shot = otherCollider.gameObject.GetComponent<ShotScript>();
if (shot != null)
{
// Avoid friendly fire
if (shot.isEnemyShot != isEnemy)
{
Damage(shot.damage);
// Destroy the shot
Destroy(shot.gameObject); // Remember to always target the game object, otherwise you will just remove the script
}
}
}
}
将“HealthScript”脚本绑定到“Poupli”的prefab上。
注意:最好直接在prefab上操作。
这样的话,场景中的每一个敌人实例都可以通过修改prefab而改动。这是非常重要的,因为在场景中我们将会有很多的敌人。
如果你是在一个游戏对象上做出改动而不是在prefab上的话,不要害怕:你可以点击“Inspector”上面的“Apply"按钮来在prefab中添加你所作出的改变。
确定子弹和Poupli在一条线上,来测试碰撞。
注意:2D物理引擎Box2D不使用Z坐标轴。Colliders 2D将会在同一个平面上,即使你的游戏对象并不是这样的。
现在运行场景,观察:
如果敌人的血量比子弹的伤害要多的话,那么敌人还是会继续存活。尝试改变敌人“HealthScript”脚本中的hp值。
开火
删除场景中的子弹。我们已经完成了它,但是现在还用不上。
我们需要一个新的脚本来开火。创建脚本“WeaponScript”。
这个脚本将会被玩家、敌人等重用。它的目的是在它附着的游戏对象前实例化一个炮弹。
这是它的整个代码,比之前的稍多一些。解释在下方:
using UnityEngine;
/// <summary>
/// Launch projectile
/// </summary>
public class WeaponScript : MonoBehaviour
{
//--------------------------------
// 1 - Designer variables
//--------------------------------
/// <summary>
/// Projectile prefab for shooting
/// </summary>
public Transform shotPrefab;
/// <summary>
/// Cooldown in seconds between two shots
/// </summary>
public float shootingRate = 0.25f;
//--------------------------------
// 2 - Cooldown
//--------------------------------
private float shootCooldown;
void Start()
{
shootCooldown = 0f;
}
void Update()
{
if (shootCooldown > 0)
{
shootCooldown -= Time.deltaTime;
}
}
//--------------------------------
// 3 - Shooting from another script
//--------------------------------
/// <summary>
/// Create a new projectile if possible
/// </summary>
public void Attack(bool isEnemy)
{
if (CanAttack)
{
shootCooldown = shootingRate;
// Create a new shot
var shotTransform = Instantiate(shotPrefab) as Transform;
// Assign position
shotTransform.position = transform.position;
// The is enemy property
ShotScript shot = shotTransform.gameObject.GetComponent<ShotScript>();
if (shot != null)
{
shot.isEnemyShot = isEnemy;
}
// Make the weapon shot always towards it
MoveScript move = shotTransform.gameObject.GetComponent<MoveScript>();
if (move != null)
{
move.direction = this.transform.right; // towards in 2D space is the right of the sprite
}
}
}
/// <summary>
/// Is the weapon ready to create a new projectile?
/// </summary>
public bool CanAttack
{
get
{
return shootCooldown <= 0f;
}
}
}
将这个脚本绑定到玩家身上。
这个脚本分为三部分:
1. “Inspector”面板中的可用变量
在这里我们有两个成员:shotPrefab和shootingRate。
第一个用于设置武器上的子弹。
在“Hierarchy”场景中选择你的玩家。在“WeaponScript”组件中你会看到“Shot Prefab”属性值是“None”。
将"Shot"prefab拖到里面:
Unity将会自动的填充这个脚本。非常简单,对吧?
shootingRate 变量有个默认值,我们暂时不会改变它。但是你可以运行这个游戏并且自己测试一下这个变量的用处。
注意:在Unity的“Inspector”中改变变量的值并不会改变脚本中的默认值。如果你把脚本添加到另一个对象上的时候,这个默认值将会是脚本的默认值。这是合乎逻辑的,但是你必须要小心。如果你想要保持调整后的值是统一的,那么你必须返回到代码部分进行改变。
2. 冷确时间
武器拥有发射速率。如果没有的话,那么你将会在每桢之内发射很多子弹。
因此我们有一个简单的冷却机制。如果大于0的话将不发射。我们在每一桢的时候我们减去运行时间。
3. 公用的攻击方法
这个脚本的主要目的是:在另一个对象中也可调用。这就是为什么我们会有一个可以创建弹药的公共方法。
一旦弹药被实例化,我们就会检索子弹对象的脚本并且重写一些变量。
注意:GetComponent()方法允许你从一个对象中获得精确的组件(也就是一个脚本,因为脚本即组件)。通用的()用来表示你所需要的确切组件。GetComponents()会得到一个组件列表而不是第一个。
玩家实体使用武器
如果你这个时候启动了游戏,那么什么都不会发生。我们虽然已经创建了武器,但目前还是无用的。
事实上,如果一个“WeaponScript”绑定到一个实体,Attack(bool)方法将不会被调用。
让我们回到“PlayScript”.
在Update()方法中,添加这个代码片段:
void Update()
{
// ...
// 5 - Shooting
bool shoot = Input.GetButtonDown("Fire1");
shoot |= Input.GetButtonDown("Fire2");
// Careful: For Mac users, ctrl + arrow is a bad idea
if (shoot)
{
WeaponScript weapon = GetComponent<WeaponScript>();
if (weapon != null)
{
// false because the player is not an enemy
weapon.Attack(false);
}
}
// ...
}
把它放在运动之前或者之后是没有关系的。
我们做了什么?
- 我们读取了开火按钮的输入(默认click或者Ctrl)。
- 我们获取了武器的脚本。
- 我们调用了Attack(false)。
按下按钮:你可以注意到我们使用GetButtonDown()方法来得到一个输入。末尾的“Down”允许我们在按钮被按下时得到这个输入,仅一次。GetButton()在每一帧都返回true直到按钮被释放。在我们的例子中,我们显然是需要GetButtonDown()方法。尝试使用GetButton()方法代替它,看看有什么不同。
按下“Play”按钮启动游戏,你将会看到:
子弹的速度太慢?尝试通过修改“Shot”prefab来得到你希望的配置。
彩蛋:仅仅是为了好玩,为玩家添加一个旋转,比如(0,0,45)。子弹将会45°方向运动,即时子弹精灵的旋转方向不是正确的,因为我们还没有改变它。
下一步
我们有了一个射手。虽然很基础,但是一个射手意味着一切。你学会了如何创造一个可以发射炮弹的武器,并且能够摧毁其他对象。
尝试去添加更多的敌人。:)
但是这部分还没有结束!我们希望敌人也可以射击。休息一下,接下来需要做的主要是重用本章中我们所做的工作。