这是一个横版射击游戏。
教程来自:
http://pixelnest.io/tutorials/2d-game-unity/
中文版教程:
http://www.litpo.com/category/易学的教程/
教程文章已经详细地介绍实现过程,这里就不再复述,只做一些简单的笔记记录,以供备忘。
1.创建第一个场景
新建工程。接着在"
Project"面板上创建如下文件夹:
接着在" Hierarchy"面板上创建多个空对象,形成以下结构:
接着在" Hierarchy"面板上创建多个空对象,形成以下结构:
这些空对象的
Position保持为(0, 0, 0)即可。保存场景,名称为"
Stage1"。
2.添加显示背景
将背景图片放入"
Textures"文件夹,确认这张图片的纹理类型,即"
Texture Type"属性为"
Sprite",如下图所示:
在场景中添加一个Sprite游戏对象,设置其" Sprite"属性为刚才导入的背景图片。重命名这个对象为" Background1",将其移动到" Level -> 0 - Background"里面,设置其" Position"为 (0, 0, 0)。接着添加背景元素。导入平台图片到 "Textures"文件夹,选中" platforms"图片,设置其" Sprite Mode"为" Multiple",然后点击" Sprite Editor",如下图所示:
在场景中添加一个Sprite游戏对象,设置其" Sprite"属性为刚才导入的背景图片。重命名这个对象为" Background1",将其移动到" Level -> 0 - Background"里面,设置其" Position"为 (0, 0, 0)。接着添加背景元素。导入平台图片到 "Textures"文件夹,选中" platforms"图片,设置其" Sprite Mode"为" Multiple",然后点击" Sprite Editor",如下图所示:
在弹出的"
Sprite Editor"窗口中,进行绘制每个平台的包围矩形,以便将纹理分割成更小的部分。然后分别命名为"
platform1"和"
platform2",如下图所示:
创建一个新的 Sprite游戏对象,设置其"Sprite"属性为" platform1"。再在创建新 Sprite对象, 设置其"Sprite"属性为" platform2"。将它们放置到" 1 - Middleground"对象里,并且确认它们的Z坐标为0。将这两个对象从" Hierarchy"面板拖动到" Project"面板下的" Prefabs"文件夹,保存成预制对象。接着,为了避免显示顺序问题,修改下游戏对象的Z坐标,如下所示:
创建一个新的 Sprite游戏对象,设置其"Sprite"属性为" platform1"。再在创建新 Sprite对象, 设置其"Sprite"属性为" platform2"。将它们放置到" 1 - Middleground"对象里,并且确认它们的Z坐标为0。将这两个对象从" Hierarchy"面板拖动到" Project"面板下的" Prefabs"文件夹,保存成预制对象。接着,为了避免显示顺序问题,修改下游戏对象的Z坐标,如下所示:
Layer | Z Position |
0 - Background | 10 |
1 - Middleground | 5 |
2 - Foreground | 0 |
此时,点击"
Scene"面板上的2D到3D视图切换,可以清楚看到层次:
3.创建主角和敌人
导入主角图片
到
"Textures"文件夹,创建一个Sprite游戏对象,命名为"Player",
设置其"Sprite"属性为刚才导入的主角图片。将此对象放入"
2 - Foreground"层里,设置其Scale为
(0.2, 0.2, 1)。接着,为主角添加盒碰撞体组件,点击"
Add Component"按钮,选择"
Box Collider 2D",设置其Size为
(10, 10),虽然这还是大于实际的区域,但已比图片小多了,如下图所示:
(可以通过按住Shift键,来调整绿色碰撞范围)
接着,再为主角对象添加"
Rigidbody 2D"刚体组件,现在运行可以看到如下结果:
可以看到主角往下落了,这是因为刚体带有重力,但在这个游戏不需要用到,将" Gravity Scale"属性设为0即可。另外,不想因为物理而引起的主角旋转,则将" Fixed Angles"属性勾选上。完整的设置如下图所示:
开始准备让主角移动。在" Scripts"文件夹里,创建一个C#脚本,名称为" PlayerScript",实现让方向键移动主角,代码如下:
这里以改变刚体的速率来达到主角的移动,而不是通过直接改变"transform.Translate",因为那样的话,可能会不产生碰撞。将此脚本附加到主角对象上,现在运行,方向键来控制移动,如下图所示:
接下来,添加第一个敌人。导入章鱼敌人图片到 "Textures"文件夹,创建一个Sprite游戏对象,命名为" Poulpi ", 设置其"Sprite"属性为刚才导入的图片,设置" Scale"属性为(0.4, 0.4, 1),添加" Box Collider 2D"组件,其 Size为(4, 4),添加" Rigidbody 2D"组件,其" Gravity Scale"属性为0,并且勾选" Fixed Angles"属性框。将对象保存为预制。在这里只让章鱼简单地往前行,创建一个脚本,命名为" MoveScript",代码如下:
可以看到主角往下落了,这是因为刚体带有重力,但在这个游戏不需要用到,将" Gravity Scale"属性设为0即可。另外,不想因为物理而引起的主角旋转,则将" Fixed Angles"属性勾选上。完整的设置如下图所示:
开始准备让主角移动。在" Scripts"文件夹里,创建一个C#脚本,名称为" PlayerScript",实现让方向键移动主角,代码如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
using UnityEngine;
/// <summary> /// Player controller and behavior /// </summary> public class PlayerScript : MonoBehaviour { /// <summary> /// 1 - The speed of the ship /// </summary> public Vector2 speed = new Vector2( 50, 50); // 2 - Store the movement private Vector2 movement; void Update() { // 3 - Retrieve axis information float inputX = Input.GetAxis( "Horizontal"); float inputY = Input.GetAxis( "Vertical"); // 4 - Movement per direction movement = new Vector2( speed.x * inputX, speed.y * inputY); } void FixedUpdate() { // 5 - Move the game object rigidbody2D.velocity = movement; } } |
接下来,添加第一个敌人。导入章鱼敌人图片到 "Textures"文件夹,创建一个Sprite游戏对象,命名为" Poulpi ", 设置其"Sprite"属性为刚才导入的图片,设置" Scale"属性为(0.4, 0.4, 1),添加" Box Collider 2D"组件,其 Size为(4, 4),添加" Rigidbody 2D"组件,其" Gravity Scale"属性为0,并且勾选" Fixed Angles"属性框。将对象保存为预制。在这里只让章鱼简单地往前行,创建一个脚本,命名为" MoveScript",代码如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
using UnityEngine;
/// <summary> /// Simply moves the current game object /// </summary> public class MoveScript : MonoBehaviour { // 1 - Designer variables /// <summary> /// Object speed /// </summary> public Vector2 speed = new Vector2( 10, 10); /// <summary> /// Moving direction /// </summary> public Vector2 direction = new Vector2(- 1, 0); private Vector2 movement; void Update() { // 2 - Movement movement = new Vector2( speed.x * direction.x, speed.y * direction.y); } void FixedUpdate() { // Apply movement to the rigidbody rigidbody2D.velocity = movement; } } |
将此脚本附加到章鱼对象上,现在运行,可以看到章鱼往前移动,如下图所示:
此时如果将主角放在章鱼前面,两个对象将会产生碰撞,互相阻塞对方的移动。
此时如果将主角放在章鱼前面,两个对象将会产生碰撞,互相阻塞对方的移动。
4.射击
导入子弹图片
到
"Textures"文件夹,创建一个Sprite游戏对象,命名为"
PlayerShot
",
设置其"Sprite"属性为刚才导入的图片
,设置"
Scale"属性为(0.75, 0.75, 1)
,添加"Rigidbody 2D"组件,其"Gravity Scale"属性为0,并且勾选"Fixed Angles"属性框
,添加"Box Collider 2D"组件,其Size为(1, 1),并且勾选"
IsTrigger"属性
。勾选"
IsTrigger"属性表示该碰撞体用于触发事件,并将被物理引擎所忽略。意味着,子弹将穿过触碰到的对象,而不会阻碍对象的移动,触碰的时候将会引发"
OnTriggerEnter2D"事件。创建一个脚本,命名为"
ShotScript",代码如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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 } } |
将此脚本附加到子弹对象上,然后将"
MoveScript"脚本也附加到子弹对象上以便可以移动。保存此对象为预制。接着,让碰撞产生伤害效果。创建一个脚本,命名为"
HealthScript",代码如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
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 } } } } |
将此脚本附加到
Poulpi预制体上。现在运行,让子弹和章鱼碰撞,可以看到如下结果:
如果章鱼的生命值大于子弹的伤害值,那么章鱼就不会被消灭,可以试着通过改变章鱼对象的" HealthScript"的hp值。接着,准备可以射击。创建一个脚本,命名为" WeaponScript",代码如下:
如果章鱼的生命值大于子弹的伤害值,那么章鱼就不会被消灭,可以试着通过改变章鱼对象的" HealthScript"的hp值。接着,准备可以射击。创建一个脚本,命名为" WeaponScript",代码如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
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; } } } |
将这个脚本附加到主角对象上,设置其"
Shot Prefab"属性为"
PlayerShot"预制体。打开"
PlayerScript"脚本,在
Update()方法里面,加入以下片段:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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); } } // ... } |
当收到射击的按钮状态,调用
Attack(false)方法。现在运行,可以看到如下结果:
接下来,准备创建敌人的子弹。 导入敌人子弹图片 到 "Textures"文件夹, 选中"PlayerShot"预制体,按下Ctrl+D进行复制,命名为"EnemyShot1",然后改变其Sprite为刚才导入的图片,设置其Scale为 (0.35, 0.35, 1)。接着,让章鱼可以射击。将" WeaponScript"脚本附加到章鱼对象上,拖动" EnemyShot1"预制体到其" Shot Prefab"属性,创建一个脚本,命名为" EnemyScript",用来简单地每一帧进行自动射击,代码如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
using UnityEngine;
/// <summary> /// Enemy generic behavior /// </summary> public class EnemyScript : MonoBehaviour { private WeaponScript weapon; void Awake() { // Retrieve the weapon only once weapon = GetComponent<WeaponScript>(); } void Update() { // Auto-fire if (weapon != null && weapon.CanAttack) { weapon.Attack( true); } } } |
将此脚本附加到章鱼对象上,如下图所示:
现在运行,可以看到如下结果:
可以看到章鱼向右射击了子弹,这是因为代码就是让它那么做的。实际上,应该做到可以朝向任何方向射击子弹。在这里,创建一个空对象,命名为" WeaponObject",删除章鱼预制体身上的" WeaponScript"脚本,将" WeaponScript"脚本附加到" WeaponObject"对象上,并且 拖动" EnemyShot1"预制体到其" Shot Prefab"属性,设置" WeaponObject"对象的" Rotation"属性为(0, 0, 180),然后保存为预制,如下所示:
接着,修改"
EnemyScript",代码为如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
using System.Collections.Generic;
using UnityEngine; /// <summary> /// Enemy generic behavior /// </summary> public class EnemyScript : MonoBehaviour { private WeaponScript[] weapons; void Awake() { // Retrieve the weapon only once weapons = GetComponentsInChildren<WeaponScript>(); } void Update() { foreach (WeaponScript weapon in weapons) { // Auto-fire if (weapon != null && weapon.CanAttack) { weapon.Attack( true); } } } } |
可以适当调整子弹的移动速度,它应该快于章鱼的移动速度。现在运行,如下图所示:
按这种方式,可以轻易实现多个方向的射击。复制添加一个" WeaponObject"对象,让这两个" WeaponObject"对象的旋转不同即可。现在主角被章鱼攻击到,但是没有任何伤害。将" HealthScript"脚本附加到主角对象上,确保" Is Enemy"属性不被勾选。现在运行,可以看到如下结果:
目前,当主角和章鱼碰撞时,仅仅只是阻碍对方的移动而已,在这里改成互相受到伤害。打开" PlayerScript"文件,添加如下代码:
按这种方式,可以轻易实现多个方向的射击。复制添加一个" WeaponObject"对象,让这两个" WeaponObject"对象的旋转不同即可。现在主角被章鱼攻击到,但是没有任何伤害。将" HealthScript"脚本附加到主角对象上,确保" Is Enemy"属性不被勾选。现在运行,可以看到如下结果:
目前,当主角和章鱼碰撞时,仅仅只是阻碍对方的移动而已,在这里改成互相受到伤害。打开" PlayerScript"文件,添加如下代码:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//PlayerScript.cs //.... void OnCollisionEnter2D(Collision2D collision) { bool damagePlayer = false; // Collision with enemy EnemyScript enemy = collision.gameObject.GetComponent<EnemyScript>(); if (enemy != null) { // Kill the enemy HealthScript enemyHealth = enemy.GetComponent<HealthScript>(); if (enemyHealth != null) enemyHealth.Damage(enemyHealth.hp); damagePlayer = true; } // Damage the player if (damagePlayer) { HealthScript playerHealth = this.GetComponent<HealthScript>(); if (playerHealth != null) playerHealth.Damage( 1); } } |
至此,一个基本的射击游戏完成。
下一篇,将进行增强表现,完善游戏。