我们的飞船面对的是可怕的章鱼,但却束手无策...
让我们授予它一把武器和一些弹药!这将包含一些脚本,但请相信,它是值得的。
首先,在让player射击前,我们需要定义一个游戏对象,用来描绘我们要用的发射物。
如下的精灵:
(右键点击保存)
发射物是我们要用得非常多的对象:当player射击时,在屏幕上将会出现很多的实例。
我们将如何处理这事儿呢?当然是Prefab
了!
你现在可以按如下步骤:
- 导入图片
- 在场景里创建一个新的
Sprite
- 将图片设置到sprite里
- 将"Sprite Layer"设置为"Bullets"
- 添加一个"Rigidbody 2D",将"Gravity Scale"和"Fixed Angles"置为0
- 添加一个"Box Collider 2D"
将绽放(scale)设置为(0.5, 0.5, 1)
,会让我们看着更舒服些。
然后,这次,我们需要在"Inspector"里设置一个新的参数:
- 在"Box Collider 2D"里,选中"Is Trigger"属性
一个trigger碰撞体,当碰撞发生时,同样会有事件,但它不会进行物理模拟。
意思是射击会在接触对象后穿过它 — 将不会再有"真实"的相互作用。同时,另一个碰撞体同样有"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
}
}
- 一些变量做为速度,方向和伤害
- 为了避免一些问题,我们将在20秒后摧毁对象
将"ShotScript"附加到精灵上,同时添加"MoveScript",这样你的射击才会移动。
拖动"Project"框里的shot游戏对象,用以创建一个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"附加到章鱼Prefab
上。
注意:直接对
Prefab
做设置是比较好的。这样做,在场景里的每个enemy实例,都会因为
Prefab
而得到修改。这里是非常重要的,因为我们在场景里有非常多的enemies。如果你只是有一个游戏对象上做设置,而不是
Prefab
,不要担心,你可以点击"Inspector"顶部的"Apply"按钮将改变保存到Prefab
里。
确保子弹和章鱼在同一条线上,测试一下。
注:2D物理引擎,Box2D,不会用到z坐标,Colliders 2D将总是在同一平面,除非你的游戏对象不在。
现在运行游戏,观察:
如果enemy的生命值大于射击的伤害值,它不会死。试着改变hp
的值,在enemy的"HealthScript":
将射击物从场景里删除,我们已经完成了它,它已经不需要了。
我们需要一个新的脚本用来开火,创建一个,命名为"WeaponScript"。
这个脚本将在到处被重复使用(playes, enemies等),它的作用是在在附着的游戏对象的前面实例化一个射击物。
这里是完整的代码,比平常多一点,解释在下面:
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;
}
}
}
将此脚本附加到player上。
此脚本分为3个部分:
我们有两个成员:shotPrefab
和shootingRate
。
第一个需要设置为我们要在武器里使用的射击物对象。
在"Hierarchy"里选择player,在"WeaponScript"组件里,你可以看到属性"Shot Prefab"是没有值的。
拖动"Shot" prefab到它上面:
Unity会自动使用此信息填充脚本,是不是很简单?
ShootingRate
变量在代码里有默认值,这次我们将不改变它。但你可以运行游戏,试着做一些测试。
小心:在Unity"Inspector"里改变变量的值,并不会改变它在脚本里的默认值,如果你将脚本添加给另外的对象,使用的仍然是脚本里的默认值
这是合理的,但你需要小心,如果你想确保调整的值,你需要在自己去代码编辑器里移植。
枪有发射频率,如果没有,在每一帧,你可能会创建成吨的射发物。
我们有一个简单的冷却原理,使用记数器,每帧减1,如果它大于0
,我们将不能射击。
这个脚本有一个主要的目的:被另外的脚本调用。这就是为什么要用公有方法来创建发射物的原因。
一但发射物实例化,we retrieve the scripts of the shot object and override some variables.
注:
GetComponent<TypeOfComponent>()
方法可以准确的获取组件对象 (同样适用于脚本,因为脚本也是一个组件),范型用于指定你想要使用的组件。
GetComponents<TypeOfComponent>()
方法会获取一组对象,而不是第一个。
在这个时候,如果你运行游戏,并没有任何的改变,我们创建一个一武器,但并没有使用。
的确,即使把"WeaponScript"附到一个实体上,但Attack(bool)
方法并没有被调用。
让我们回到"PlayerScript"。
在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()
来代替,观察它们的不同
运行游戏:
子弹太慢了?尝试找到"Shot" prefab的设置项,并设置为你喜欢的值。
红利:好玩,给player增加一个倾斜度,像(0, 0, 45)
,射击将会有一个45度的移动,但射击物的旋转角度并不正确,因为我们也没有改变它。
我们有了射手!一个非常基础的版本,但毕竟是射手。你学会了如何创建武器,并可以开火摧毁目标,试着添加更多的敌人。:)
但这一部分还没有结束!我们想enemies也可以射击,休息一下,接下来,我们主要是利用刚才我们做的内容。