Unity开启篇(十三) —— Unity脚本简介(二)

版本记录

版本号 时间
V1.0 2019.01.26 星期六

前言

Unity是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。Unity类似于Director,Blender game engine, Virtools 或 Torque Game Builder等利用交互的图型化开发环境为首要方式的软件。其编辑器运行在Windows 和Mac OS X下,可发布游戏至WindowsMacWiiiPhoneWebGL(需要HTML5)、Windows phone 8和Android平台。也可以利用Unity web player插件发布网页游戏,支持Mac和Windows的网页浏览。它的网页播放器也被Mac 所支持。网页游戏 坦克英雄和手机游戏王者荣耀都是基于它的开发。
下面我们就一起开启Unity之旅。感兴趣的看下面几篇文章。
1. Unity开启篇(一) —— Unity界面及创建第一个简单的游戏 (一)
2. Unity开启篇(二) —— Unity界面及创建第一个简单的游戏 (二)
3. Unity开启篇(三) —— 一款简单射击游戏示例 (一)
4. Unity开启篇(四) —— 一款简单射击游戏示例 (二)
5. Unity开启篇(五) —— 一款简单射击游戏示例 (三)
6. Unity开启篇(六) —— Unity动画简介 (一)
7. Unity开启篇(七) —— Unity动画简介 (二)
8. Unity开启篇(八) —— Unity声音简介(一)
9. Unity开启篇(九) —— Unity声音简介(二)
10. Unity开启篇(十) —— Unity粒子系统简介(一)
11. Unity开启篇(十一) —— Unity粒子系统简介(二)
12. Unity开启篇(十二) —— Unity脚本简介(一)

Working with Prefabs

简单地跑来跑去避开敌人是一个非常片面的游戏。 是时候武装玩家进行战斗了。

单击“层次结构”中的Create按钮,然后选择3D Object/Capsule。 将其命名为Projectile并为其提供以下变换值:

  • 1) Position: (0, 0, 0)
  • 2) Rotation: (90, 0, 0)
  • 3) Scale: (0.075, 0.246, 0.075)

每次玩家射击时,它都会发射一枚Projectile。 要实现这一点,您需要创建一个预制件(Prefab)。 与场景中已有的对象不同,预制件是由游戏逻辑按需创建的。

Assets下创建一个名为Prefabs的新文件夹。 现在将Projectile对象拖动到此文件夹中。 就是这样:你有一个预制件!

你的预制件需要一些脚本。 在名为ProjectileScripts文件夹中创建一个新脚本,并向其添加以下类变量:

public float speed;
public int damage;

Vector3 shootDirection;

就像本教程中到目前为止的任何移动物体一样,这个也会有速度和伤害speed and damage变量,因为它是战斗逻辑的一部分。 shootDirection向量确定了弹丸(Projectile)的去向。

通过在类中实现以下方法使该向量起作用:

// 1
void FixedUpdate () {
  this.transform.Translate(shootDirection * speed, Space.World);
}

// 2
public void FireProjectile(Ray shootRay) {
  this.shootDirection = shootRay.direction;
  this.transform.position = shootRay.origin;
}

// 3
void OnCollisionEnter (Collision col) {
  Enemy enemy = col.collider.gameObject.GetComponent<Enemy>();
  if(enemy) {
    enemy.TakeDamage(damage);
  }
  Destroy(this.gameObject);
}

以下是上述代码中的内容:

  • 1) 弹丸的移动方式与此游戏中的其他所有方式不同。 它没有目标,或者随着时间推移施加一些力量;相反,它在整个生命周期中沿预定方向移动。
  • 2) 在这里设置预制件的起始位置和方向。 这个Ray看起来很神秘,但你很快就会知道它的计算方法。
  • 3) 如果射弹与敌人发生碰撞,它会调用TakeDamage()并自行摧毁。

在场景层次结构中,将Projectile脚本附加到Projectile GameObject。 将Speed设置为0.2并将Damage设置为1,然后单击位于Inspector顶部附近的Apply按钮。 这将应用您刚刚对此预制件的所有实例所做的更改。

从场景层次结构中移除投射(Projectile)物体——你不再需要它了。


Firing Projectiles

现在你有一个预制件可以移动和应用伤害,你准备好开始射击。

Player文件夹中,创建一个名为PlayerShooting的新脚本,并将其附加到场景中的Player。在类内部,声明以下变量:

public Projectile projectilePrefab;
public LayerMask mask;

第一个变量将包含对您之前创建的Projectile Prefab的引用。每次你的玩家发射射弹时,你都会从这个预制件中创建一个新实例。mask变量用于过滤GameObjects

在你的游戏中有时候你需要知道对撞是否存在于特定的方向。为此,Unity可以将不可见光线从某个点投射到您指定的方向。您可能会遇到许多与光线相交的游戏对象,因此使用遮罩可以过滤掉任何不需要的对象。

Raycast非常有用,可用于各种用途。它们通常用于测试其他玩家是否被射弹击中,但您也可以使用它们来测试鼠标指针下方是否有任何几何形状。要了解有关Raycast的更多信息,请在Unity站点上查看此Unity live training video

下图显示了从立方体到圆锥的光线投射。由于光线上有一个图标球面罩,它会忽略该GameObect并报告锥体上的命中:

现在是时候发射自己的光线了。

将以下方法添加到PlayerShooting.cs

void shoot(RaycastHit hit){
  // 1
  var projectile = Instantiate(projectilePrefab).GetComponent<Projectile>();
  // 2
  var pointAboveFloor = hit.point + new Vector3(0, this.transform.position.y, 0);

  // 3
  var direction = pointAboveFloor - transform.position;

  // 4
  var shootRay = new Ray(this.transform.position, direction);
  Debug.DrawRay(shootRay.origin, shootRay.direction * 100.1f, Color.green, 2);

  // 5
  Physics.IgnoreCollision(GetComponent<Collider>(), projectile.GetComponent<Collider>());

  // 6
  projectile.FireProjectile(shootRay);
}

以下是上述代码的作用:

  • 1) 实例化弹丸Prefab并获得其弹丸(Projectile)组件,以便初始化。
  • 2) 这一点总是看起来像(x,0.5,z)。 X和Z是平面上从鼠标点击位置投射光线的坐标。这个计算很重要,因为(Projectile)必须与地面平行 - 否则你将向下射击,只有业余射击者才能射向地面。
  • 3) 计算从Player GameObjectpointAboveFloor的方向。
  • 4) 创建一条新射线,通过其原点和方向描述射弹轨迹。
  • 5) 这条线告诉Unity的物理引擎忽略了Player colliderProjectile collider之间的碰撞。否则,在它有机会飞行之前,将调用Projectile脚本中的OnCollisionEnter()
  • 6) 最后,它设定了射弹的轨迹。

注意:在光线投射非常宝贵时使用Debug.DrawRay(),因为它可以帮助您可视化光线的外观和击中的内容。

使用触发逻辑,添加以下方法让玩家实际拉动触发器:

// 1
void raycastOnMouseClick () {
  RaycastHit hit;
  Ray rayToFloor = Camera.main.ScreenPointToRay(Input.mousePosition);
  Debug.DrawRay(rayToFloor.origin, rayToFloor.direction * 100.1f, Color.red, 2);

  if(Physics.Raycast(rayToFloor, out hit, 100.0f, mask, QueryTriggerInteraction.Collide)) {
    shoot(hit);
  }
}

// 2
void Update () {
  bool mouseButtonDown = Input.GetMouseButtonDown(0);
  if(mouseButtonDown) {
    raycastOnMouseClick();  
  }
}

下面细分:

  • 1) 此方法将光线从相机投射到鼠标单击的位置。 然后它检查此光线是否与给定的LayerMask的游戏对象相交。
  • 2) 每次更新时,脚本都会检查鼠标左键是否按下。 如果找到一个,则调用raycastOnMouseClick()

返回Unity并在Inspector中设置以下变量:

  • Projectile Prefab:从预制文件夹中引用Projectile
  • Mask:平面

注意:Unity附带了有限数量的预定义图层,您可以从中创建蒙版。

您可以通过单击GameObjectLayer下拉列表并选择Add Layer来创建自己的:

要将图层指定给GameObject,请从图层下拉列表中选择它:

有关图层的更多信息,请查看Unity's Layers documentation

运行项目并随意开火! 射弹向所需的方向发射,但似乎有点不对,不是吗?

如果射弹指向行进方向,那将会更加Cool。 要解决此问题,请打开Projectile.cs脚本并添加以下方法:

void rotateInShootDirection() {
  Vector3 newRotation = Vector3.RotateTowards(transform.forward, shootDirection, 0.01f, 0.0f);
  transform.rotation = Quaternion.LookRotation(newRotation);
}

注意:RotateTowardsMoveTowards非常相似,但它将向量视为方向而不是位置。 此外,您不需要随时间更改旋转,因此使用接近零的步骤就足够了。 Unity中的变换旋转使用四元数(quaternions)表示,这超出了本教程的范围。 您在本教程中需要了解的是,在进行涉及3D旋转的计算时,它们优于矢量。

有兴趣了解有关四元数的更多信息以及为什么它们有用吗? 看看这篇优秀的文章:How I learned to Stop Worrying and Love Quaternions

FireProjectile()的末尾,添加对rotateInShootDirection()的调用。 FireProjectile()现在应该如下所示:

public void FireProjectile(Ray shootRay) {
  this.shootDirection = shootRay.direction;
  this.transform.position = shootRay.origin;
  rotateInShootDirection();
}

再次运行游戏并向几个不同的方向开火;这次射弹将指向他们射击的方向:

删除Debug.DrawRay调用,因为您将不再需要它们。


Generating More Bad Guys

只有一个敌人并不是非常具有挑战性。 但是既然你了解了Prefabs,你就可以产生你想要的所有对手!

为了让玩家猜测,你可以随机化每个敌人的生命,速度和位置。

创建一个空的游戏对象 - GameObject \ Create Empty。 将其命名为EnemyProducer并添加Box Collider组件。 在Inspector中设置值,如下所示:

  • 1)Position: (0, 0, 0)
  • 2)Box Collider:
    • Is Trigger: true
    • Center: (0, 0.5, 0)
    • Size: (29, 1, 29)

您附加的对撞定义了竞技场内的特定3D空间。 要查看此内容,请在层次结构中选择Enemy Producer GameObject,然后在Scene视图中查看:

The green wire outlines represent a collider

您将编写一个脚本,沿X和Z轴在此空间中选择一个随机位置,并实例化一个敌人预制件。

创建一个名为EnemyProducer的新脚本,并将其附加到EnemyProducer GameObject。 在新设置的类中,添加以下实例成员:

public bool shouldSpawn;
public Enemy[] enemyPrefabs;
public float[] moveSpeedRange;
public int[] healthRange;

private Bounds spawnArea;
private GameObject player;

第一个变量启用和禁用spawning。 该脚本将从enemyPrefabs中挑选一个随机的敌人预制件并实例化它。 接下来的两个数组将指定速度和生命值的最小值和最大值。 生成区域是您在“场景”视图中看到的绿色框。 最后,你需要一个对玩家的引用并将其作为目标传递给坏人。

在脚本内部,定义以下方法:

public void SpawnEnemies(bool shouldSpawn) {
  if(shouldSpawn) {
    player = GameObject.FindGameObjectWithTag("Player");
  }
  this.shouldSpawn = shouldSpawn;
}

void Start () {
  spawnArea = this.GetComponent<BoxCollider>().bounds;
  SpawnEnemies(shouldSpawn);
  InvokeRepeating("spawnEnemy", 0.5f, 1.0f);
}

SpawnEnemies()获取带有标记Player的游戏对象的引用,并确定敌人是否应该生成。

Start()初始化生成区域并在游戏开始后0.5秒调度方法的调用。 它会每秒重复调用一次。 除了充当setter方法之外,SpawnEnemies()还获得带有标记Player的游戏对象的引用。

Player游戏对象尚未标记 - 您现在就可以执行此操作。 从Hierarchy中选择Player对象,然后在Inspector选项卡中,从Tag下拉菜单中选择Player

现在,您需要为单个敌人编写实际的产卵代码。

打开Enemy script并添加以下方法:

public void Initialize(Transform target, float moveSpeed, int health) {
  this.targetTransform = target;
  this.moveSpeed = moveSpeed;
  this.health = health;
}

这只是作为创建对象的setter。 接下来:产生你的敌人联盟的代码。 打开EnemyProducer.cs并添加以下方法:

Vector3 randomSpawnPosition() {
  float x = Random.Range(spawnArea.min.x, spawnArea.max.x);
  float z = Random.Range(spawnArea.min.z, spawnArea.max.z);
  float y = 0.5f;

  return new Vector3(x, y, z);
}

void spawnEnemy() {
  if(shouldSpawn == false || player == null) {
    return;
  }

  int index = Random.Range(0, enemyPrefabs.Length);
  var newEnemy = Instantiate(enemyPrefabs[index], randomSpawnPosition(), Quaternion.identity) as Enemy;
  newEnemy.Initialize(player.transform, 
      Random.Range(moveSpeedRange[0], moveSpeedRange[1]), 
      Random.Range(healthRange[0], healthRange[1]));
}

spawnEnemy()所做的就是选择一个随机敌人预制件,在随机位置实例化它并初始化敌人脚本公共变量。

EnemyProducer.cs几乎准备好了!

返回Unity。 通过将Enemy对象从Hierarchy拖动到Prefabs文件夹来创建Enemy预制件。 从场景中移除敌人对象 - 你不再需要它了。 接下来设置Enemy Producer脚本公共变量,如下所示:

  • 1) Should Spawn: True
  • 2) Enemy Prefabs:
    • Size: 1
    • Element 0: Reference the enemy prefab
  • 3) Move Speed Range:
    • Size: 2
    • Element 0: 3
    • Element 1: 8
  • 4) Health Range:
    • Size: 2
    • Element 0: 2
    • Element 1: 6

运行游戏并查看它 - 无穷无尽的坏人!

好吧,那些立方体看起来并不十分可怕。 是时候把事情搞清楚了。

在场景中创建3D CylinderCapsule。 分别将它们命名为Enemy2Enemy3。 正如您之前对第一个敌人所做的那样,将两个Rigidbody组件和Enemy script添加到它们中。 选择Enemy2并在Inspector中更改其配置,如下所示:

  • 1) Scale: (0, 0.5, 0)
  • 2) Rigidbody:
    • Use Gravity: False
    • Freeze Position: Y
    • Freeze Rotation: X, Y, Z
  • 3) Enemy Component:
    • Move Speed: 5
    • Health: 2
    • Damage: 1
    • Target Transform: None

现在对Enemy3做同样的事情,但是将它的Scale设置为0.7:

接下来,将它们变成Prefabs,就像你对原始敌人一样,并在Enemy Producer中引用所有这些。 检查器中的值应如下所示:

  • Enemy Prefabs:
    • Size: 3
    • Element 0: Enemy
    • Element 1: Enemy2
    • Element 2: Enemy3

运行游戏,你会看到不同的预制件在竞技场内产生。

不久之后你就会意识到自己是无敌的! 这很棒,你需要稍微调整一下比赛场地。


Implementing the Game Controller

既然你有射击,移动和敌人,你将实现一个基本的游戏控制器。 一旦Player“死”,它将重新开始游戏。 但首先,您必须创建一种机制来通知任何感兴趣的玩家已达到0健康状态。

打开Player script并在类声明上面添加以下内容:

using System;

在类内添加以下新的公共事件:

public event Action<Player> onPlayerDeath;

事件是一种C#语言功能,允许您将对象的更改广播到任何监听者。 要了解如何使用活动,请查看Unity's live training on events.

编辑collidedWithEnemy()如下:

void collidedWithEnemy(Enemy enemy) {
  enemy.Attack(this);
  if(health <= 0) {
    if(onPlayerDeath != null) {
      onPlayerDeath(this);
    }
  }
}

事件为对象提供了一种简洁的方式来表示它们之间的状态变化。 游戏控制器会对上面声明的事件非常感兴趣。 在Scripts文件夹中,创建一个名为GameController的新脚本。 双击该文件进行编辑,并添加以下变量:

public EnemyProducer enemyProducer;
public GameObject playerPrefab;

脚本需要对敌人的生产有一些控制权,因为一旦玩家死亡,产生敌人是没有意义的。 此外,重新启动游戏意味着您必须重新创建Player,这意味着......这是正确的,它将成为预制件。

添加以下方法:

void Start () {
  var player = GameObject.FindGameObjectWithTag("Player").GetComponent<Player>();
  player.onPlayerDeath += onPlayerDeath;
}

void onPlayerDeath(Player player) {
  enemyProducer.SpawnEnemies(false);
  Destroy(player.gameObject);

  Invoke("restartGame", 3);
}

Start()中,脚本获取对Player脚本的引用,并订阅您之前创建的事件。 一旦玩家的生命值达到0,将调用onPlayerDeath(),停止敌人生产,从场景中移除Player对象并在3秒后调用restartGame()方法。

最后,添加重启游戏动作的实现:

void restartGame() {
  var enemies = GameObject.FindGameObjectsWithTag("Enemy");
  foreach (var enemy in enemies)
  {
    Destroy(enemy);
  }

  var playerObject = Instantiate(playerPrefab, new Vector3(0, 0.5f, 0), Quaternion.identity) as GameObject;
  var cameraRig = Camera.main.GetComponent<CameraRig>();
  cameraRig.target = playerObject;
  enemyProducer.SpawnEnemies(true);
  playerObject.GetComponent<Player>().onPlayerDeath += onPlayerDeath;
}

在这里你做了一些清理工作:你摧毁场景中的所有敌人并创建一个新的Player对象。 然后,您将摄像机装备的目标重新分配给此实例,恢复敌方的产生,并将Game Controller订阅到玩家死亡事件。

现在返回Unity,打开Prefabs文件夹,将所有Enemy预制件的标签更改为Enemy。 接下来,将Player游戏对象拖入Prefabs文件夹中,使其成为Prefab。 创建一个空的游戏对象,将其命名为GameController并附加刚刚创建的脚本。 在Inspector中连接所有必需的引用。

到现在为止,你对这种模式非常熟悉。 尝试自己放置引用,然后根据下面隐藏的插图检查结果:

Game Controller:

  • Enemy Producer:来自层次结构的敌人生产者引用
  • Player Prefab:从Prefabs文件夹中引用它

再次运行游戏以查看game controller的运行情况。

就是这样 - 你编写了第一个Unity游戏!恭喜!

到现在为止,你应该很好地理解将一个简单的动作游戏结合起来所需要的东西。制作游戏不是一项简单的任务;它绝对需要大量工作,脚本只是将项目变为现实所需的元素之一。要增加良好的润色程度,您还需要为游戏添加动画和UI。

后记

本篇主要讲述了Unity脚本简介,感兴趣的给个赞或者关注~~~

推荐阅读更多精彩内容