目录

概述

一、准备工作

二、开始创建步骤

1、创建“Player”

说明:“Player”为游戏场景中的一个对象,在本游戏中就是一个有C#脚本控制的具有智慧的球体。

2、创建“Player” 移动脚本

3、创建相机脚本

4、创建敌人

5、使用预制体

6、发射弹丸

7、生成更多敌人

8、实现游戏控制器

总结



概述

通过创建经典的竞技场射击游戏来学习 Unity 中脚本的基本知识。本示例是Georgi Ivanov发表的英文文章的翻译,是学习Unity 应用C#脚本的很好学习资料,作为初学者请依该文方法练习一遍,保证您对Unity脚本编程已经掌握。

最终效果图

Unity 的很多力量都来自其丰富的脚本语言C#。您可以使用它来处理用户输入、操作场景中的对象、检测碰撞、生成新的 GameObject 和在场景周围投射方向光线,以帮助处理游戏逻辑。这听起来可能令人生畏,但 Unity 公开了记录良好的 API,这些 API 使这些任务变得轻而易举 — 即使对于新手开发人员!在本教程中,您将创建一个自上而下的射击游戏,该游戏使用 Unity 脚本来处理敌人的生成、玩家控制、发射弹丸和游戏的其他重要方面。

注意:本教程是为 Unity 5.3 或更高版本编写的。您可以在此处下载最新版本的Unity。虽然 Unity 也支持 UnityScript 和 Boo,但 C# 是大多数开发人员倾向于使用的编程语言,并且有充分的理由。C# 被全球数百万开发人员用于应用程序、Web 和游戏开发,并且有大量的信息和教程可以帮助您。


一、准备工作

下载GameSample初学者项目,解压缩,并在 Unity 中打开创建的文件夹,您应该看到:

"Scene view"视图中有一个小竞技场,这将是游戏的战场,一个相机和一个光源。如果您的布局与屏幕截图中不同,请选择右上拉菜单,然后更改为Default。

二、开始创建步骤


1、创建“Player”

"Hierarchy"中,单击"Create"按钮,然后从 3D 部分中选择" Sphere"。将 Sphere定位在 (X:0, Y:0.5, Z:0),并命名“Player”:

Unity 使用entity-component来构建其游戏对象。这意味着所有 GameObject 都是组件的容器,可以附加这些容器来赋予其行为和属性。下面是 Unity 内置的组件的一些示例:

Tranform每个游戏对象都附带这个组件。它持有游戏对象的位置、旋转和比例。

Box Collider立方体形状的碰撞器,可用于检测碰撞。

Mesh Filter:用于显示 3D 模型的网格数据。

“Player”游戏对象将需要响应与场景中其他对象的碰撞。为此,请在"Hierarchy"窗口中选择“Player”,然后在"Inspector "窗口中的单击"Add Component"按钮,在弹出的菜单中选择"Physics > Rigidbody",这将向“Player”添加一个刚体组件,以便它可以使用 Unity 的物理引擎。调整 Rigidody 的值,像这样:将拖动设置为 1,将角拖动设置 0,并选中"Freeze Position"旁边的 Y复选框。这将确保“Player”无法上下移动,并且在旋转时没有添加阻尼。

2、创建“Player” 移动脚本

现在,“Player” 已经准备好了,是时候创建脚本,从键盘开始输入并移动播放机了。在"Project"窗口中,单击" Create"按钮并选择" Folder"。命名新文件夹Scripts并在其中创建一个名为“Player” 的子文件夹。在“Player” 文件夹中,单击"Create"按钮并选择"C#Scripts"。命名您的新脚本PlayerMovement。步骤如下所示:

注:使用这样的文件夹可以方便地按角色组织所有内容,并减少混乱。您将制作多个脚本供“Player”使用,因此向它提供自己的文件夹是有意义的。

击PlayerMovement.cs脚本。这将打开加载脚本时的首选代码编辑器。Unity 附带了在所有程序上预安装的 MonoDevelop,Windows 用户可以选择安装 Visual Studio来取代它,并在运行安装程序时使用。本教程假定您使用的是 MonoDevelop,但 Visual Studio 用户应该能够毫无问题。一旦您选择的编辑器打开,您将看到:

这是 Unity 在新脚本中生成的默认的类。它继承了 MonoBehaviour 基类,这样脚本才能够在游戏中运行,同时还有一些特殊的方法对特定事件作出响应。如果你是一个 iOS 开发者,这个类就好比 UIViewCotnroller。Unity 会在运行脚本时以特定顺序调用多个方法。最常见的几个方法包括:

  • Start():在脚本进行第一次更新之前,将调用此方法一次。
  • Update():当游戏正在运行并启用脚本时,此方法将触发每帧。
  • OnDestroy():在游戏对象附加到此脚本被销毁之前,将调用此方法。
  • OnCollisionEnter():当对撞机或刚体连接到其他碰撞器或刚体时,将调用此方法。

在 Start() 方法前,添加两行代码:

<span style="color:#ffffff"><span style="color:#cc99cd">public</span> <span style="color:#f08d49">float</span> acceleration;
<span style="color:#cc99cd">public</span> <span style="color:#f08d49">float</span> maxSpeed;</span>

这是它应该的格式:

这意味着这两个变量能够在检视器中看到并修改,而无需在脚本和编辑器中来回切换。acceleration 表示玩家的速度随着时间递增。maxSpeed 则表示速度的上限。在它们后面声明这几个变量:

<span style="color:#ffffff"><span style="color:#cc99cd">private</span> Rigidbody rigidBody;
<span style="color:#cc99cd">private</span> KeyCode[] inputKeys;
<span style="color:#cc99cd">private</span> Vector3[] directionsForKeys;</span>

不能通过检查器设置私有变量,开发人员有责任在适当的时间初始化它们。rigidBody 用于保存一个对刚体组件的引用,即附着在 Player GameObject 上的刚体组件。

inputKeys 是一个键盘码的数组,用于检查输入。directionsForKeys 用于保存一个 Vector3 变量数组,这些变量表示方向数据。将 Start() 方法修改为:

<span style="color:#ffffff"><span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">Start</span> ()</span> {inputKeys = <span style="color:#cc99cd">new</span> KeyCode[] { KeyCode.W, KeyCode.A, KeyCode.S, KeyCode.D };directionsForKeys = <span style="color:#cc99cd">new</span> Vector3[] { Vector3.forward, Vector3.left, Vector3.back, Vector3.right };rigidBody = GetComponent<Rigidbody>();
}</span>

此代码链接每个键的相应方向,例如按W将对象向前移动。最后一行获得了一个对所附着的刚体组件的引用,将它保存到 rigidBody 变量以便使用。要真正移动玩家的角色,还需要处理键盘输入。将 Update() 修改为 FixedUpdate() 并加入以下代码:

<span style="color:#ffffff"><span style="color:#999999">// 1</span>
<span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">FixedUpdate</span> ()</span> {<span style="color:#cc99cd">for</span> (<span style="color:#f08d49">int</span> i = <span style="color:#f08d49">0</span>; i < inputKeys.Length; i++){<span style="color:#cc99cd">var</span> key = inputKeys[i];<span style="color:#999999">// 2</span><span style="color:#cc99cd">if</span>(Input.GetKey(key)) {<span style="color:#999999">// 3</span>Vector3 movement = directionsForKeys[i] * acceleration * Time.deltaTime;}}
}</span>

这里有几件重要的事情:

  1. FixedUpdate()速率与帧速率无关,它和刚体一起使用。和以尽量快的速度运行不同,这个方法会以固定的间隔执行。
  2. 此循环检查是否按下了有效键。
  3. 获取按下键的方向,将其乘以加速度和完成最后一帧的秒数。这将生成用于移动对象的方向矢量(X、Y 和 Z 轴上的速度)。就可以用它来移动 Player 对象了。

如果您是游戏编程的新朋友,您可能会问自己为什么必须乘以Time.detalTime。虽然游戏运行帧速率(或帧每秒)会因硬件和承受的压力而异,但这可能会导致在功能强大的计算机上发生太快,在较弱的机器上速度过慢,这可能会导致不希望发生的行为。通常的办法是,当需要按每帧执行一个动作时,都乘上 Time.deltaTimeTime.

在 FixedUpdate() 方法后添加:

<span style="color:#ffffff"><span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">movePlayer</span>(<span style="color:#f08d49">Vector3 movement</span>)</span> {<span style="color:#cc99cd">if</span>(rigidBody.velocity.magnitude * acceleration > maxSpeed) {rigidBody.AddForce(movement * <span style="color:#f08d49">-1</span>);} <span style="color:#cc99cd">else</span> {rigidBody.AddForce(movement);}
}</span>

这个方法向刚体施加一个力,驱使它移动。乳沟当前速度超过 maxSpeed,这个力会转成反方向,让玩家减速,将速度有效地限制在最大速度下。在 FixedUpdate() 方法中,在 if 语句右括号结束之前,添加:

<span style="color:#ffffff">movePlayer(movement);</span>

完美!保存此脚本并返回到 Unity 编辑器。在"Project"窗口中,将"PlayerMovement"脚本拖动到层次结构内的 Player上。将脚本添加到 GameObject 将创建一个组件的实例,这意味着您附加的 GameObject 执行所有代码。使用检查将 Acceleration 设置为 625,将Max Speed 设置为 4375

运行场景,使用 WASD 键移动 Player:

对于只有几行代码来说, 这是一个相当不错的结果!然而,有一个明显的问题——Player迅速移动到视线外,这使得它很难打到敌人。

3、创建相机脚本

在脚本编辑器中,新建脚本名为 CameraRig,然后将它添加到 MainCamera。

<span style="color:#ffffff"><span style="color:#cc99cd">public</span> <span style="color:#f08d49">float</span> moveSpeed;
<span style="color:#cc99cd">public</span> GameObject target;<span style="color:#cc99cd">private</span> Transform rigTransform;</span>

你可能想到了,moveSpeed 是相机跟随目标——任何场景内部游戏对象——进行移动的速度。在 Start() 方法中,添加:

<span style="color:#ffffff">rigTransform = <span style="color:#cc99cd">this</span>.transform.parent;</span>

这句引用了父对象 Camera 在场景树中的 transform 组件。每个在场景中的对象都会有一个 Transform 组件,它描述了对象的位置、角度和比例。

在同一脚本中,添加以下方法:

<span style="color:#ffffff"><span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">FixedUpdate</span> ()</span> {<span style="color:#cc99cd">if</span>(target == <span style="color:#f08d49">null</span>){<span style="color:#cc99cd">return</span>;}rigTransform.position = Vector3.Lerp(rigTransform.position, target.transform.position, Time.deltaTime * moveSpeed);
}</span>

CameraRig 的移动代码比 PlayerMovement 要简单。这是因为你不需要刚体,在 rigTransform 和 target 的位置之间做插值运算即可。Vector3.Lerp() 以两个点和一个0-1 之间的小数做参数,这个小数表示两个端点之间的一个位置。左端点为 0,右端点为 1。0.5 则返回两点之间的终点。以渐慢方式让 rigTransform 靠近 target 的位置。也就是说——相机会跟随玩家角色。回到 Unity。在结构视图选中 Main Camera。在检视器中,设置 Move Speed 为 8 ,Target 为 Player:

运行游戏,并在现场移动;摄像机无论走到哪里,都应平稳地跟随目标变换。

4、创建敌人

没有敌人的射击游戏很容易被击败, 但有点无聊。通过单击顶部菜单中的GameObject+3D 对象+立方体来创建敌人立方体。将多维数据集重命名为"敌人"并添加刚体组件。在检查器中,首先将多维数据集的变换设置为(0, 0.5,4)。在刚零部件的"约束"部分中,选中"冻结位置"类别中的"Y"复选框。

现在让敌人以一种吓人的方式移动吧。在 Scripts 目录下新建脚本 Enemy。这个步骤你应该很熟悉了,如果忘记了,请参考前面的描述过的步骤。然后,在类中声明变量:

<span style="color:#ffffff"><span style="color:#cc99cd">public</span> <span style="color:#f08d49">float</span> moveSpeed;
<span style="color:#cc99cd">public</span> <span style="color:#f08d49">int</span> health;
<span style="color:#cc99cd">public</span> <span style="color:#f08d49">int</span> damage;
<span style="color:#cc99cd">public</span> Transform targetTransform;</span>

这些变量的作用并不难猜。moveSpeed 先前在相机中也用到过,这里是同样的作用。health 和 damage 用于决定敌人什么时候死,以及它们对玩家造成的伤害。targetTransform 引用了玩家的 transform。对于 Player 来说,你需要一个类描述玩家的所有属性,这一切恰好是敌人想摧毁的。在项目浏览器中,选中 Player 文件夹并新建脚本 Player,这个脚本用于对碰撞进行处理,并保存玩家的生命值。双击脚本,打开它。添加一个公共变量用于保存玩家的生命值:

<span style="color:#ffffff"><span style="color:#cc99cd">public</span> <span style="color:#f08d49">int</span> health = <span style="color:#f08d49">3</span>;</span>

这为health提供了默认值,但也可以在检查器中修改。要处理冲突,请添加以下方法:

<span style="color:#ffffff"><span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">collidedWithEnemy</span>(<span style="color:#f08d49">Enemy enemy</span>)</span> {<span style="color:#999999">// Enemy attack code</span><span style="color:#cc99cd">if</span>(health <= <span style="color:#f08d49">0</span>) {<span style="color:#999999">// Todo </span>}
}<span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">OnCollisionEnter</span> (<span style="color:#f08d49">Collision col</span>)</span> {Enemy enemy = col.collider.gameObject.GetComponent<Enemy>();collidedWithEnemy(enemy);
}</span>

OnCollisionEnter() 方法会在两个带有碰撞体的刚体发生碰撞时触发。Collision 参数包含了交点和碰撞速度等信息。在这里,你只对 Collision 中的 Enemy 组件感兴趣,因此调用 collidedWithEnemy() 并执行攻击逻辑——这个在后面添加。回到 Enemy.cs,添加下列方法:

<span style="color:#ffffff"><span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">FixedUpdate</span> ()</span> {<span style="color:#cc99cd">if</span>(targetTransform != <span style="color:#f08d49">null</span>) {<span style="color:#cc99cd">this</span>.transform.position = Vector3.MoveTowards(<span style="color:#cc99cd">this</span>.transform.position, targetTransform.transform.position, Time.deltaTime * moveSpeed);}
}<span style="color:#70a2ff"><span style="color:#cc99cd">public</span> <span style="color:#cc99cd">void</span> <span style="color:#70a2ff">TakeDamage</span>(<span style="color:#f08d49"><span style="color:#f08d49">int</span> damage</span>)</span> {health -= damage;<span style="color:#cc99cd">if</span>(health <= <span style="color:#f08d49">0</span>) {Destroy(<span style="color:#cc99cd">this</span>.gameObject);}
}<span style="color:#70a2ff"><span style="color:#cc99cd">public</span> <span style="color:#cc99cd">void</span> <span style="color:#70a2ff">Attack</span>(<span style="color:#f08d49">Player player</span>)</span> {player.health -= <span style="color:#cc99cd">this</span>.damage;Destroy(<span style="color:#cc99cd">this</span>.gameObject);
}</span>

FixedUpdate() 方法你应该很熟悉了,略有不同的地方是,你用 MoveToward() 替代了 Lerp() 方法。这是因为敌人始终以同样的速度进行移动,当它到达目标后不需要减速。当敌人被子弹击中,TakeDamage() 方法被调用;当敌人的生命值变为 0,它将被销毁。Attack() 是类似的——它将伤害施加到 Player,然后敌人自动销毁。回到 Player.cs,在 collidedWithEnemy() 方法中,将注释“Enemy attack code”替换成:

<span style="color:#ffffff">enemy.Attack(<span style="color:#cc99cd">this</span>);</span>

在这个过程中,玩家被减血,敌人自毁。

返回 Unity。将 Enemy 脚本绑定到 Enemy 对象,在检视器中,修改 Enemy 的属性:

  1. Move Speed: 5
  2. Health: 2
  3. Damage: 1
  4. Target Transform: Player

现在你应该自己尝试着修改这些值。自己动手,然后和下面的 Gif 动画进行比较:

在这个游戏中,当敌人和玩家发生碰撞,就会构成一次攻击。用 Unity 的物理引擎来检测碰撞不过是小菜一碟。最终,将 Player 脚本绑定到结构视图中的 Player 上。运行游戏,注意查看控制台:

当敌人碰上玩家,它会进行攻击并扣减玩家的生命值为 2。但控制台会抛出一个 NullReferenceException 错误,指向了 Player 脚本的这一行:

哎呀——玩家不仅仅和敌人发生了碰撞,也和游戏中的其它对象发生了碰撞,比如竞技场。因为这个对象没有 Enemy 脚本,因此 GetComponent() 返回了 null。打开 Player.cs。在 OnCollisionEnter() 方法中,用一个 if 语句将 collidedWithEnemy() 方法包裹起来:

<span style="color:#ffffff"><span style="color:#cc99cd">if</span>(enemy) {collidedWithEnemy(enemy);
}</span>

不再有空号!

5、使用预制体

简单地跑来跑去,避免敌人是一个相当片面的游戏。是时候武装玩家战斗了。

单击层次结构中的"创建"按钮并选择3D 对象/胶囊。命名它 Projectile并赋予它以下转换值:

  1. 位置: (0, 0, 0)
  2. 旋转: (90, 0, 0)
  3. 比例: (0.075, 0.246, 0.075)

每次玩家拍摄时,它将发射 的实例。为此,您需要创建 一个 。与场景中已有的对象不同,预制件是按游戏逻辑按需创建的。ProjectilePrefab

在"资产"下创建一个名为"预制件"的新文件夹。现在"弹丸"对象拖动到此文件夹中。就是这样: 你有一个预制件!

您的预制件需要一点脚本。在名为 Projectile 的脚本文件夹中创建一个新脚本,并将其添加到其中的类变量:

<span style="color:#ffffff"><span style="color:#cc99cd">public</span> <span style="color:#f08d49">float</span> speed;
<span style="color:#cc99cd">public</span> <span style="color:#f08d49">int</span> damage;Vector3 shootDirection;</span>

就像本教程中到目前为止的任何移动对象一样,这个对象也将具有速度和伤害变量,因为它是战斗逻辑的一部分。矢量确定 将去哪里。shootDirectionProjectile

通过将该向量在类中实现以下方法,请将该向量用于工作:

<span style="color:#ffffff"><span style="color:#999999">// 1</span>
<span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">FixedUpdate</span> ()</span> {<span style="color:#cc99cd">this</span>.transform.Translate(shootDirection * speed, Space.World);
}<span style="color:#999999">// 2</span>
<span style="color:#70a2ff"><span style="color:#cc99cd">public</span> <span style="color:#cc99cd">void</span> <span style="color:#70a2ff">FireProjectile</span>(<span style="color:#f08d49">Ray shootRay</span>)</span> {<span style="color:#cc99cd">this</span>.shootDirection = shootRay.direction;<span style="color:#cc99cd">this</span>.transform.position = shootRay.origin;
}<span style="color:#999999">// 3</span>
<span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">OnCollisionEnter</span> (<span style="color:#f08d49">Collision col</span>)</span> {Enemy enemy = col.collider.gameObject.GetComponent<Enemy>();<span style="color:#cc99cd">if</span>(enemy) {enemy.TakeDamage(damage);}Destroy(<span style="color:#cc99cd">this</span>.gameObject);
}</span>

以下是上述代码中正在执行的:

  1. 移动不同于这个游戏中其他一切。它没有目标,或随着时间的推移施加到它一些力量;相反,它在整个生命周期中朝着预定的方向发展。Projectile
  2. 在这里,您可以设置预制件的起始位置和方向。这个论点似乎很神秘,但你很快就会知道它是如何计算的。Ray
  3. 如果一个弹丸与敌人相撞,它会调用并摧毁自己。TakeDamage()

在场景层次结构中,将弹射脚本附加到弹射游戏对象。将"速度"设置为 0.2,"伤害"设置为1,然后单击位于检查器顶部附近的"应用"按钮。这将应用您刚才所做的更改到此预制件的所有实例。

场景层次结构中删除"弹射"对象 - 不再需要它。

6、发射弹丸

现在,您有一个可以移动和应用伤害的预制件,您就可以开始拍摄了。

在"播放器"文件夹中,创建一个名为"播放器拍摄"的新脚本并将其附加到场景中的播放机。在类中,声明以下变量:

<span style="color:#ffffff"><span style="color:#cc99cd">public</span> Projectile projectilePrefab;
<span style="color:#cc99cd">public</span> LayerMask mask;</span>

第一个变量将包含对之前创建的弹射预制件的引用。每次播放机发射弹丸时,您都会从此预制件创建新实例。该变量用于筛选游戏对象。mask

等等, 铸造光线?这是什么巫术?

不, 没有黑魔法在进行 - 有时在你的游戏中, 当你需要知道, 如果碰撞器存在于一个特定的方向。为此,Unity 可以从特定点投射到指定方向的不可见光线。您可能会遇到许多与光线相交的 Game 对象,因此使用掩码可以过滤掉任何不需要的对象。

射线广播非常有用,可用于各种用途。它们通常用于测试其他玩家是否被弹丸击中,但您也可以使用它们来测试鼠标指针下方是否有任何几何体。要了解有关 Raycasts 的信息,请查看 Unity网站上的此 Unity实时培训视频。

下图显示了从立方体到圆锥体的光线投射。由于光线上有一个图标球蒙版,因此它忽略游戏对象并报告圆锥体上的命中:

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

将以下方法添加到PlayerShooting.cs

<span style="color:#ffffff"><span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">shoot</span>(<span style="color:#f08d49">RaycastHit hit</span>)</span>{<span style="color:#999999">// 1</span><span style="color:#cc99cd">var</span> projectile = Instantiate(projectilePrefab).GetComponent<Projectile>();<span style="color:#999999">// 2</span><span style="color:#cc99cd">var</span> pointAboveFloor = hit.point + <span style="color:#cc99cd">new</span> Vector3(<span style="color:#f08d49">0</span>, <span style="color:#cc99cd">this</span>.transform.position.y, <span style="color:#f08d49">0</span>);<span style="color:#999999">// 3</span><span style="color:#cc99cd">var</span> direction = pointAboveFloor - transform.position;<span style="color:#999999">// 4</span><span style="color:#cc99cd">var</span> shootRay = <span style="color:#cc99cd">new</span> Ray(<span style="color:#cc99cd">this</span>.transform.position, direction);Debug.DrawRay(shootRay.origin, shootRay.direction * <span style="color:#f08d49">100.1f</span>, Color.green, <span style="color:#f08d49">2</span>);<span style="color:#999999">// 5</span>Physics.IgnoreCollision(GetComponent<Collider>(), projectile.GetComponent<Collider>());<span style="color:#999999">// 6</span>projectile.FireProjectile(shootRay);
}</span>

以下是上述代码所起的用:

  1. 实例化弹丸预制件并获取其组件,以便可以初始化它。Projectile
  2. 这一点总是看起来像 。X 和 Z 是地板上的坐标,从鼠标单击位置投射光线。这种计算很重要,因为弹丸必须平行于地面,否则你会向下射击,只有业余爱好者才能向地面射击。:](x, 0.5, z)
  3. 计算从玩家游戏对象到 的方向。pointAboveFloor
  4. 创建一条新光线,按其原点和方向描述弹丸轨迹。
  5. 此行告诉 Unity 的物理引擎忽略 和 之间的碰撞。否则在将调用之前,它有机会飞走。Player colliderProjectile colliderOnCollisionEnter()Projectile script
  6. 最后,它为弹丸设定了轨迹。

注:当光线投射是无价的时,它可以帮助您可视化光线的外观和击中什么。Debug.DrawRay()

在启动逻辑到位后,添加以下方法,让玩家实际扣动扳机:

<span style="color:#ffffff"><span style="color:#999999">// 1</span>
<span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">raycastOnMouseClick</span> ()</span> {RaycastHit hit;Ray rayToFloor = Camera.main.ScreenPointToRay(Input.mousePosition);Debug.DrawRay(rayToFloor.origin, rayToFloor.direction * <span style="color:#f08d49">100.1f</span>, Color.red, <span style="color:#f08d49">2</span>);<span style="color:#cc99cd">if</span>(Physics.Raycast(rayToFloor, <span style="color:#cc99cd">out</span> hit, <span style="color:#f08d49">100.0f</span>, mask, QueryTriggerInteraction.Collide)) {shoot(hit);}
}<span style="color:#999999">// 2</span>
<span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">Update</span> ()</span> {<span style="color:#f08d49">bool</span> mouseButtonDown = Input.GetMouseButtonDown(<span style="color:#f08d49">0</span>);<span style="color:#cc99cd">if</span>(mouseButtonDown) {raycastOnMouseClick();  }
}</span>

将每个编号注释重新进行:

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

返回到 Unity 并在检查器中设置以下变量:

  • 弹射预制件:从预制件文件夹中引用弹丸
  • 掩码:地板

注:Unity 附带有限的预定义图层,您可以在其中创建蒙版。

您可以通过单击"游戏对象"的"图层"下拉列表并选择"添加图层"来创建自己的图层:

要将图层分配给游戏对象,请从"图层"下拉列表中选择它:

有关图层的详细信息,请查看 Unity的图层文档。

运行项目,并着火的要从!弹丸是朝所需方向发射的,但有些东西似乎有点脱落,不是吗?

如果弹丸指向行驶方向,会凉快得多。若要解决此问题,请打开Projectile.cs并添加以下方法:

<span style="color:#ffffff"><span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">rotateInShootDirection</span>()</span> {Vector3 newRotation = Vector3.RotateTowards(transform.forward, shootDirection, <span style="color:#f08d49">0.01f</span>, <span style="color:#f08d49">0.0f</span>);transform.rotation = Quaternion.LookRotation(newRotation);
}</span>

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

有兴趣了解更多有关四元纪元以及为什么它们有用?看看这篇优秀的文章: 我是如何学会停止担心和爱四重奏的

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

<span style="color:#ffffff"><span style="color:#70a2ff"><span style="color:#cc99cd">public</span> <span style="color:#cc99cd">void</span> <span style="color:#70a2ff">FireProjectile</span>(<span style="color:#f08d49">Ray shootRay</span>)</span> {<span style="color:#cc99cd">this</span>.shootDirection = shootRay.direction;<span style="color:#cc99cd">this</span>.transform.position = shootRay.origin;rotateInShootDirection();
}</span>

再次运行游戏,在几个不同的方向开火;这一次,弹丸将指向他们拍摄的方向:

删除呼叫,因为您不需要进一步呼叫。Debug.DrawRay

7、生成更多敌人

只有一个敌人并不是非常具有挑战性。但现在你知道了预制件,你可以生成所有你想要的对手!:]

为了保持玩家猜测,您可以随机化每个敌人的运行状况、速度和位置。

创建空游戏对象 -游戏对象 =创建空。将其命名为"敌人"生产器并添加盒式对撞器组件。在检查器中设置值,如下所示:

  1. 位置: (0, 0, 0)
  2. 盒式碰撞器:
    • 是触发器:正真
    • 中心: (0, 0.5, 0)
    • 大小: (29, 1, 29)

您附加的对撞机定义竞技场内的特定 3D 空间。若要查看此内容,请在层次结构中选择"游戏对象"并查看场景视图内部:Enemy Producer

绿色导线轮廓表示对撞机。

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

创建一个名为"敌人制作者"的新脚本,并将其附加到敌人制作者游戏对象。在新设置的类中,添加以下实例成员:

<span style="color:#ffffff"><span style="color:#cc99cd">public</span> <span style="color:#f08d49">bool</span> shouldSpawn;
<span style="color:#cc99cd">public</span> Enemy[] enemyPrefabs;
<span style="color:#cc99cd">public</span> <span style="color:#f08d49">float</span>[] moveSpeedRange;
<span style="color:#cc99cd">public</span> <span style="color:#f08d49">int</span>[] healthRange;<span style="color:#cc99cd">private</span> Bounds spawnArea;
<span style="color:#cc99cd">private</span> GameObject player;</span>

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

在脚本中,定义以下方法:

<span style="color:#ffffff"><span style="color:#70a2ff"><span style="color:#cc99cd">public</span> <span style="color:#cc99cd">void</span> <span style="color:#70a2ff">SpawnEnemies</span>(<span style="color:#f08d49"><span style="color:#f08d49">bool</span> shouldSpawn</span>)</span> {<span style="color:#cc99cd">if</span>(shouldSpawn) {player = GameObject.FindGameObjectWithTag(<span style="color:#7ec699">"Player"</span>);}<span style="color:#cc99cd">this</span>.shouldSpawn = shouldSpawn;
}<span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">Start</span> ()</span> {spawnArea = <span style="color:#cc99cd">this</span>.GetComponent<BoxCollider>().bounds;SpawnEnemies(shouldSpawn);InvokeRepeating(<span style="color:#7ec699">"spawnEnemy"</span>, <span style="color:#f08d49">0.5f</span>, <span style="color:#f08d49">1.0f</span>);
}</span>

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

Start()初始化生成区域,并在游戏开始后 0.5 秒计划调用方法。它会每隔一秒被反复调用。除了充当设置器方法外,还可以获取具有 标记的游戏对象的引用。SpawnEnemies()Player

玩家游戏对象尚未标记 - 您现在将这样做。从"层次结构"中选择"播放者"对象,然后在"检查器"选项卡中选择"播放者",从"标记"下拉菜单中选择"播放者":

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

打开"敌人"脚本并添加以下方法:

<span style="color:#ffffff"><span style="color:#70a2ff"><span style="color:#cc99cd">public</span> <span style="color:#cc99cd">void</span> <span style="color:#70a2ff">Initialize</span>(<span style="color:#f08d49">Transform target, <span style="color:#f08d49">float</span> moveSpeed, <span style="color:#f08d49">int</span> health</span>)</span> {<span style="color:#cc99cd">this</span>.targetTransform = target;<span style="color:#cc99cd">this</span>.moveSpeed = moveSpeed;<span style="color:#cc99cd">this</span>.health = health;
}</span>

这仅充当创建对象的设置器。下一步: 代码生成你的敌人联盟。打开EnemyProducer.cs并添加以下方法:

<span style="color:#ffffff"><span style="color:#70a2ff">Vector3 <span style="color:#70a2ff">randomSpawnPosition</span>()</span> {<span style="color:#f08d49">float</span> x = Random.Range(spawnArea.min.x, spawnArea.max.x);<span style="color:#f08d49">float</span> z = Random.Range(spawnArea.min.z, spawnArea.max.z);<span style="color:#f08d49">float</span> y = <span style="color:#f08d49">0.5f</span>;<span style="color:#cc99cd">return</span> <span style="color:#cc99cd">new</span> Vector3(x, y, z);
}<span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">spawnEnemy</span>()</span> {<span style="color:#cc99cd">if</span>(shouldSpawn == <span style="color:#f08d49">false</span> || player == <span style="color:#f08d49">null</span>) {<span style="color:#cc99cd">return</span>;}<span style="color:#f08d49">int</span> index = Random.Range(<span style="color:#f08d49">0</span>, enemyPrefabs.Length);<span style="color:#cc99cd">var</span> newEnemy = Instantiate(enemyPrefabs[index], randomSpawnPosition(), Quaternion.identity) <span style="color:#cc99cd">as</span> Enemy;newEnemy.Initialize(player.transform, Random.Range(moveSpeedRange[<span style="color:#f08d49">0</span>], moveSpeedRange[<span style="color:#f08d49">1</span>]), Random.Range(healthRange[<span style="color:#f08d49">0</span>], healthRange[<span style="color:#f08d49">1</span>]));
}</span>

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

EnemyProducer.cs差不多准备好了!

回到团结。通过将"敌人"对象从层次结构拖动到"预制件"文件夹来创建"敌人"预制件。从场景中移除敌人对象 - 您不再需要它了。接下来设置脚本公共变量,如:Enemy Producer

  1. 应生成:
  2. 敌人预制件:
    • 大小: 1
    • 元素 0:参考敌人的预制件
  3. 移动速度范围:
    • 大小: 2
    • 元素 0: 3
    • 元素 1: 8
  4. 健康范围:
    • 大小: 2
    • 元素 0: 2
    • 元素 1: 6

运行游戏,并检查出来 - 一无尽的坏人流!

好吧,那些立方体看起来并不可怕。是时候把事情调味了。

在场景中创建3D圆柱体和胶囊。分别命名他们敌人2和敌人3。正如您之前对第一个敌人所做的一样,向它们添加一个刚体组件和"敌人"脚本。选择"敌人2"并在检查器中更改其配置,请按以下操作进行:

  1. 比例: (0, 0.5, 0)
  2. 刚体:
    • 使用重力:
    • 冻结位置:Y
    • 冻结旋转:X、 Y、 Z
  3. 敌人组件:
    • 移动速度: 5
    • 健康: 2
    • 伤害: 1
    • 目标变换:没有

现在对敌人3也做同样的工作,但将其比例设置为0.7:

接下来,将它们变成预制件,就像您使用原始敌人一样,并在《敌人生产者》中引用所有这些。检查器中的值应看起来像:

  • 敌人预制件:

    • 大小: 3
    • 元素 0:敌人
    • 元素1:敌人2
    • 元素2:敌人3

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

过没多久就意识到自己无敌了!虽然很棒, 但你需要提高一点竞争环境。

8、实现游戏控制器

现在,你有射击,运动和敌人到位,你将实现一个基本的游戏控制器。一旦 "死", 它将重新启动游戏。但首先,您必须创建一个机制,通知任何感兴趣的各方,玩家已达到 0 运行状况。Player

打开播放机脚本并在类声明上方添加以下内容:

<span style="color:#ffffff"><span style="color:#cc99cd">using</span> System;</span>

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

<span style="color:#ffffff"><span style="color:#cc99cd">public</span> <span style="color:#cc99cd">event</span> Action<Player> onPlayerDeath;</span>

事件一个 C# 语言功能,允许您向任何侦听器广播对象中的更改。要了解如何使用活动,请查看 Unity的活动现场培训。 .

编辑可查看以下代码:collidedWithEnemy()

<span style="color:#ffffff"><span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">collidedWithEnemy</span>(<span style="color:#f08d49">Enemy enemy</span>)</span> {enemy.Attack(<span style="color:#cc99cd">this</span>);<span style="color:#cc99cd">if</span>(health <= <span style="color:#f08d49">0</span>) {<span style="color:#cc99cd">if</span>(onPlayerDeath != <span style="color:#f08d49">null</span>) {onPlayerDeath(<span style="color:#cc99cd">this</span>);}}
}</span>

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

<span style="color:#ffffff"><span style="color:#cc99cd">public</span> EnemyProducer enemyProducer;
<span style="color:#cc99cd">public</span> GameObject playerPrefab;</span>

脚本将需要对敌人的生产有一些控制,因为一旦玩家灭亡,生成敌人就没有意义。此外,重新启动游戏意味着你将不得不重新创建播放器,这意味着...没错, 它会成为预制件。

添加以下方法:

<span style="color:#ffffff"><span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">Start</span> ()</span> {<span style="color:#cc99cd">var</span> player = GameObject.FindGameObjectWithTag(<span style="color:#7ec699">"Player"</span>).GetComponent<Player>();player.onPlayerDeath += onPlayerDeath;
}<span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">onPlayerDeath</span>(<span style="color:#f08d49">Player player</span>)</span> {enemyProducer.SpawnEnemies(<span style="color:#f08d49">false</span>);Destroy(player.gameObject);Invoke(<span style="color:#7ec699">"restartGame"</span>, <span style="color:#f08d49">3</span>);
}</span>

在 中,脚本获取对脚本的引用,并订阅您之前创建的事件。一旦玩家的运行状况达到 0 将被调用,停止敌人生产,从场景中删除播放机对象并在 3 秒后调用方法。Start()PlayeronPlayerDeath()restartGame()

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

<span style="color:#ffffff"><span style="color:#70a2ff"><span style="color:#cc99cd">void</span> <span style="color:#70a2ff">restartGame</span>()</span> {<span style="color:#cc99cd">var</span> enemies = GameObject.FindGameObjectsWithTag(<span style="color:#7ec699">"Enemy"</span>);<span style="color:#cc99cd">foreach</span> (<span style="color:#cc99cd">var</span> enemy <span style="color:#cc99cd">in</span> enemies){Destroy(enemy);}<span style="color:#cc99cd">var</span> playerObject = Instantiate(playerPrefab, <span style="color:#cc99cd">new</span> Vector3(<span style="color:#f08d49">0</span>, <span style="color:#f08d49">0.5f</span>, <span style="color:#f08d49">0</span>), Quaternion.identity) <span style="color:#cc99cd">as</span> GameObject;<span style="color:#cc99cd">var</span> cameraRig = Camera.main.GetComponent<CameraRig>();cameraRig.target = playerObject;enemyProducer.SpawnEnemies(<span style="color:#f08d49">true</span>);playerObject.GetComponent<Player>().onPlayerDeath += onPlayerDeath;
}</span>

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

现在返回到 Unity,打开预制件文件夹,并更改所有敌人预制件的标签到敌人。接下来,通过将玩家游戏对象拖动到"预制件"文件夹中,将其制作成预制件。创建一个空的游戏对象,将其命名为 GameController并附加您刚刚创建的脚本。将检查器中所有必需的引用挂钩。

现在,你非常熟悉这种模式。尝试自己放置参考资料,然后根据下面隐藏的插图检查结果:

再次运行游戏以查看游戏控制器在运行。

就是这样 - 你已经脚本了你的第一个 Unity 游戏!祝贺!

该处使用的url网络请求的数据。


总结

您可以在此处下载已完成的项目。现在,你应该掌握了如何构建一个简单的动作游戏。游戏制作不是简单工作;要完成整个游戏,大量的工作和脚本绝对只是其中一部分。为了添加更多的亮点,你必须在游戏中添加动画和 UI。

Unity3D开发体验之Unity3D 脚本开发相关推荐

  1. Polyworks脚本开发学习笔记(一)-脚本开发环境

    Polyworks脚本开发学习笔记(一)-脚本开发环境 背景 Polyworks的扫描尺寸测量分析模块是我工作中经常用到的一个模块,我不是做测量的,但是利用Polyworks对扫描获得的点云来进行尺寸 ...

  2. illustrator插件--常用功能开发--色标生成器--js脚本开发--AI插件

      illustrator是矢量编辑软件,画板是绘制处理的重要容器,在印刷方面的一个重要功能就是色标生成器,开发一个色标生成器功能,以下功能仅用于学习交流,请勿用于非法用途和商业用途,源代码如下所示: ...

  3. illustrator插件-画板功能开发-插入页码-js脚本开发-ai插件

    文章目录 1.算法程序 2.文本转执行 3.作者答疑 1.算法程序   illustrator是矢量编辑软件,画板是绘制处理的重要容器,开发一个插入页码功能,源代码如下所示: if (app.docu ...

  4. illustrator插件--常用功能开发--绘制手提袋--js脚本开发--AI插件

      illustrator是矢量编辑软件,画板是绘制处理的重要容器,在印刷方面的一个重要功能就是绘制手提袋,开发一个绘制手提袋功能,以下功能仅用于学习交流,请勿用于非法用途和商业用途,源代码如下所示: ...

  5. AfterEffect插件--常规功能开发--扭曲效果--js脚本开发--AE插件

    文章目录 1.扭曲效果功能 2.作者答疑 1.扭曲效果功能   AfterEffect(AE)插件是Adobe公司开发的特效制作软件,稳定快速的功能和特效,在视频制作领域使用非常广泛,本文向大家介绍如 ...

  6. AfterEffect(AE)插件-常规功能开发-创建面板-js脚本开发

    文章目录 1.算法程序 2.作者答疑 1.算法程序   AfterEffect(AE)插件是特效制作软件,本文向大家介绍如何开发插件中的子模块创建面板.源代码如下所示: {function DemoP ...

  7. illustrator插件-拼版功能开发-阵列复制-js脚本开发-ai插件

    文章目录 1.算法程序 2.作者答疑 1.算法程序   illustrator是矢量编辑软件,画板是绘制处理的重要容器,在印刷方面的一个重要功能就是拼版,开发一个阵列复制功能,以下功能仅用于学习交流, ...

  8. InDesign插件--常规功能开发--版面调整--js脚本开发--ID插件

    文章目录 1.算法程序 2.作者答疑 1.算法程序   Adobe InDesign是Adobe公司的一个桌面出版 (DTP) 的应用程序,主要用于各种印刷品的排版编辑.InDesign可以将文档直接 ...

  9. Qlik sense开发入门/可视化BI报表开发入门

    最近开发公司的BI可视化报表,接触了Qlik报表工具,使用过程中发现网上入门教程很少,因此写一篇入门开发经历.啥也不说,先上一个已经开发完成的需求中的其中一个展示页面截图: 如上图, Qlik这个工具 ...

最新文章

  1. (送)Java 架构技术揭秘:Redis+Nginx+Dubbo精选+面试题+精选视频
  2. 3.6.1 局域网的基本概念和体系结构
  3. python File 内置 open()方法(打开文件)
  4. Angular中数据文本绑定、绑定Html、绑定属性、双向数据绑定的实现方式
  5. SCUT - 337 - 岩殿居蟹 - 线段树 - 树状数组
  6. java获取mavenlib路径,maven install报错致命错误: 在类路径或引导类路径中找不到程序包 java.lang | ZPY博客...
  7. 手机 服务器 推送消息推送消息,推送信息到手机的pushover使用方法及sample code
  8. leetcode869. 重新排序得到 2 的幂
  9. div中文字,图片居中对齐,兼容FF、IE
  10. Kubernetes学习总结(10)—— 何为云原生,与 kubernetes 是什么关系
  11. pytorch如何将训练提速?
  12. 操作基本数据类型的流/数据流
  13. 玉伯的一道课后题题解(关于 IEEE 754 双精度浮点型精度损失)
  14. 计算机三级数据库技术复习资料总结
  15. 全国java二级考试范围,全国计算机二级Java考试大纲
  16. Hi,大家好,这里是iOS用的 Flash 播放器 FlashViewer
  17. 新员工入职表_员工离职率过高,只要三步骤,就能轻松有效控制
  18. 快速实战SQL (一) - 检索数据
  19. 伴随着我娃成长的运维平台(图片持续更新..)
  20. 横向评测常见的优秀国外5个域名注册商

热门文章

  1. 米奇诺娃,让我读到了历史的真相。
  2. Exchange邮件pst数据的导出与查看
  3. 第五章:稍息!立正!——print格式化输出,痴月熊学python
  4. 关于推广京东商品拿佣金的方法,类似于淘宝客
  5. SAN和NAS之间到底有什么区别?
  6. 「GoCN酷Go推荐」重试工具 — retry-go
  7. c语言的野指针以及如何避免野指针
  8. chrome浏览器无法访问百度
  9. 10年计算机速度慢加固态硬盘行不行,电脑越来越慢?你的ssd固态硬盘分区弄好了吗...
  10. ubuntu 20.04.1 自动关机