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粒子系统简介(二)

开始

在本教程中,通过创建一个经典的竞技场射击游戏来学习Unity中脚本的基础知识。

Unity的大部分功能都在于其丰富的脚本语言C#。 您可以使用它来处理用户输入,操纵场景中的对象,检测碰撞,生成新的游戏对象以及在场景周围投射定向光线以帮助您的游戏逻辑。 这可能听起来令人生畏,但Unity公开了具有完备文档的API,使这些任务变得轻而易举 - 即使对于新手开发人员也是如此!

在本教程中,您将创建一个自上而下的射击游戏,它使用Unity脚本来处理敌人的产生,玩家控制,射击射弹以及游戏玩法的其他重要方面。

注意:本教程假设您已具备使用C#或类似编程语言的经验,并了解Unity的界面和工作流程。

本教程是为Unity 5.3或更高版本编写的。 您可以在此处here下载最新版本的Unity。

虽然Unity也支持UnityScriptBoo,但C#是大多数开发人员倾向于使用的编程语言,并且有充分的理由。 C#被全球数百万开发人员用于应用程序,Web和游戏开发,并且有大量的信息和教程可以帮助您。

打开已有的工程,具体页面如下所示:

Scene view视图中查看。 这是一个小型区域,将成为游戏的战场,相机和灯光。 如果您的布局与屏幕截图中的布局不同,请选择右上角的下拉菜单并将其更改为2 by 3

没有英雄角色算什么游戏? 您的第一个任务是创建一个GameObject来表示场景中的玩家。


Creating the Player

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

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

  • Tranform:每个GameObject都附带此组件。 它保存GameObject的位置,旋转和缩放。
  • Box Collider:立方体形状的对撞机,可用于探测碰撞。
  • Mesh Filter:用于显示3D模型的网格数据。

Player GameObject需要响应与场景中其他对象的碰撞。

要实现此目的,请在Hierarchy window窗口中选择Player,然后单击“检查器”窗口中的Add `Component按钮。 在弹出的菜单中选择Physics> Rigidbody,这将为播放器添加一个Rigidbody组件,以便它可以使用Unity的物理引擎。

像这样调整Rigidody的值:设置Drag到1,Angular Drag到0并选中Freeze Position旁边的Y复选框。

这将确保Player无法上下移动,并且在旋转时不会增加阻尼。


Creating the Player Movement Script

现在Player已准备就绪,是时候创建将从键盘输入并移动Player的脚本。

Project window中,单击Create按钮并选择Folder。 将新文件夹命名为Scripts,并在其中创建一个名为Player的子文件夹。
Player文件夹中,单击Create按钮并选择C#Script。 将新脚本命名为PlayerMovement。 序列如下所示:

注意:使用这样的文件夹可以轻松地按角色组织所有内容并减少混乱。 你将为Player制作几个脚本,所以给它自己的文件夹是有意义的。

双击PlayerMovement.cs脚本。 这将打开您首选的代码编辑器并加载脚本。 Unity附带预装在所有平台上的MonoDevelopWindows用户可以选择安装Visual Studio,并在运行安装程序时使用它。

本教程假设您使用的是MonoDevelop,但Visual Studio用户应该能够顺利进行操作而不会出现任何问题。

一旦您选择的编辑器打开,您将看到以下界面:

这是Unity为新脚本生成的默认类。它源自基类MonoBehaviour,它确保此脚本将在游戏循环中运行,并具有对某些事件作出反应的附加功能。如果你来自iOS世界,这个对象相当于UIViewController。当脚本运行时,Unity会按预定顺序调用多个方法。以下是一些最常见的:

  • Start():在脚本第一次更新之前,将调用此方法一次。
  • Update():当游戏运行且脚本启用时,此方法将在每一帧被触发。
  • OnDestroy():此方法在附加到此脚本的GameObject之前被调用。
  • OnCollisionEnter():当连接此脚本的碰撞器或刚体触及另一个碰撞器或刚体时,此方法被调用。

有关事件的完整列表,请查看Unity’s documentation on MonoBehaviours的文档。

Start()方法上方添加以下两行:

public float acceleration;
public float maxSpeed;

它应该是这样的:

这些是公共变量声明(variable declarations),这意味着这些变量将在Inspector中可见,并且可以调整而无需在脚本和编辑器之间来回切换。

acceleration描述了Player's速度随着时间的推移而增加的程度。 maxSpeed是“速度限制”。

在其下方,声明以下变量:

private Rigidbody rigidBody;
private KeyCode[] inputKeys;
private Vector3[] directionsForKeys;

私有变量无法通过Inspector设置,开发人员有责任在适当的时候初始化它们。
rigidBody将保存对附加到Player GameObjectRigidbody组件的引用。

inputKeys是一个用于检测输入的密钥代码数组。
directionsForKeys包含一个Vector3变量数组,它将保存方向数据。

用以下内容替换Start()方法:

void Start () {
  inputKeys = new KeyCode[] { KeyCode.W, KeyCode.A, KeyCode.S, KeyCode.D };
  directionsForKeys = new Vector3[] { Vector3.forward, Vector3.left, Vector3.back, Vector3.right };
  rigidBody = GetComponent<Rigidbody>();
}

这段代码链接每个密钥的相应方向,例如 按W可将物体向前移动。 最后一行获取对atttached Rigidbody组件的引用,并将其保存在rigidBody变量中供以后使用。

要实际移动Player,您必须处理键盘输入。

Update()重命名为FixedUpdate()并添加以下代码:

// 1
void FixedUpdate () {
  for (int i = 0; i < inputKeys.Length; i++){
    var key = inputKeys[i];

    // 2
    if(Input.GetKey(key)) {
      // 3
      Vector3 movement = directionsForKeys[i] * acceleration * Time.deltaTime;
    }
  }
}

这里有几件重要的事情:

  • 1) FixedUpdate()与帧速率无关,在使用Rigidbodies时应该使用。此方法将以恒定间隔触发,而不是尽可能快地运行。
  • 2) 此循环检查是否按下了任何输入键。
  • 3) 获取按下的键的方向,将其乘以加速度和完成最后一帧所花费的秒数。这将生成一个方向向量(X,Y和Z轴上的速度),用于移动Player对象。

如果您不熟悉游戏编程,您可能会问自己为什么必须乘以Time.deltaTime。当游戏运行时,帧速率(或每秒帧数)将根据硬件和它所承受的压力而变化,这可能导致在强大的机器上发生很快,而在较弱的机器上发生太慢可能导致不期望的行为。一般规则是当你每个(固定)帧执行一个动作时,你需要乘以Time.deltaTime

FixedUpdate()下面添加以下方法:

void movePlayer(Vector3 movement) {
  if(rigidBody.velocity.magnitude * acceleration > maxSpeed) {
    rigidBody.AddForce(movement * -1);
  } else {
    rigidBody.AddForce(movement);
  }
}

上述方法对ridigbody施加力,使其移动。 如果当前速度超过maxSpeed,则力量向相反方向移动以减慢玩家的速度,并有效地限制最大速度。

FixedUpdate()中,在if语句的右括号之前,添加以下行:

movePlayer(movement);

很好! 保存此脚本并返回Unity编辑器。 在Project window中,将PlayerMovement脚本拖动到“层次结构”内的Player上。

GameObject添加脚本会创建组件的实例,这意味着将为您附加的GameObject执行所有代码。

使用InspectorAcceleration设置为625,将Max Speed设置为4375

运行场景并使用WASD键移动Player

只有几行代码,这是一个非常好的结果!

然而,有一个明显的问题 - 玩家可以快速移出视线。


Creating the Camera Script

Scripts文件夹中,创建一个名为CameraRig的新脚本,并将其附加到Main Camera。 需要一些帮助来弄清楚步骤? 您可以查看下面的提示。

选择Scripts文件夹后,单击项目浏览器(Project Browser)中的Create按钮,然后选择C#Script。 将新脚本命名为CameraRig。 最后,将其拖放到Main Camera对象上,如下所示:

现在,在新创建的CameraRig类中,在Start()方法的正上方创建以下变量:

public float moveSpeed;
public GameObject target;

private Transform rigTransform;

您可能已经猜到了,moveSpeed是摄像机跟随目标的速度 - 可以是场景中的任何游戏对象。

Start()内,添加以下行:

rigTransform = this.transform.parent;

此代码获取对场景层次结构中父Camera对象的变换的引用。 场景中的每个对象都有一个变换Transform,它描述了对象的位置,旋转和缩放。

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

void FixedUpdate () {
  if(target == null){
    return;
  }

  rigTransform.position = Vector3.Lerp(rigTransform.position, target.transform.position, 
    Time.deltaTime * moveSpeed);
}

CameraRig移动代码比PlayerMovement中的移动代码稍微简单一些。 这是因为你不需要Rigidbody;只需在rigTransform和目标的位置之间进行插值即可。

Vector3.Lerp()在空间中取两个点,在[0,1]范围内取浮点,它描述了沿两个端点的点。 左端点为0,右端点为1。将0.5传递给Lerp()将在两个端点之间准确返回一个点。

这会使rigTransform更接近目标位置并稍微缓和一下。 简而言之 - 相机跟随player

回到Unity。 确保在层次结构中仍然选择了Main Camera。 在Inspector中,将Move Speed设置为8,将Target设置为Player

运行游戏并在场景中移动;无论走到哪里,摄像机都应该顺利地跟踪目标变换。


Creating an Enemy

没有敌人的射击游戏很容易被击败,但有点无聊。 通过单击顶部菜单中的GameObject \ 3D Object \ Cube创建敌人立方体。 将您的Cube重命名为Enemy并添加Rigidbody组件。

Inspector中,首先将CubeTransform设置为(0,0.5,4)。 在Rigidbody组件的Constraints部分中,选中Freeze Position类别中的Y复选框。

优秀 - 现在让你的敌人以威胁的方式四处走动。 在Scripts文件夹中创建一个名为Enemy的脚本。 您现在应该成为专家,但如果没有,请查看教程前面的说明以供参考。

接下来,在类中添加以下公共变量:

public float moveSpeed;
public int health;
public int damage;
public Transform targetTransform;

您可能无需太多困难就能弄清楚这些变量代表什么。 您之前使用moveSpeed创建了摄像机装备,它在此处具有相同的效果。 healthdamage有助于确定敌人何时死亡以及他们的死亡会对Player造成多大伤害。 最后,targetTransform引用了Player的变换。

说到玩家,你需要创建一个类来表示敌人想要摧毁的所有玩家。

在项目浏览器中,选择Player文件夹并创建一个名为Player的新脚本;此脚本将对碰撞做出反应并跟踪玩家的健康状况。 双击脚本进行编辑。

添加以下公共变量以存储Player's的生命值:

public int health = 3;

这为健康状况提供了默认值,但也可以在检查器中进行修改。

要处理碰撞,请添加以下方法:

void collidedWithEnemy(Enemy enemy) {
  // Enemy attack code
  if(health <= 0) {
    // Todo 
  }
}

void OnCollisionEnter (Collision col) {
    Enemy enemy = col.collider.gameObject.GetComponent<Enemy>();
    collidedWithEnemy(enemy);
}

当两个带有碰撞器的刚体接触时,OnCollisionEnter()会触发。 Collision参数包含有关接触点和碰撞速度等信息。 在这种情况下,您只对碰撞对象的Enemy组件感兴趣,因此您可以调用collidedWithEnemy()并执行攻击逻辑 - 您接下来将添加该逻辑。

切换回Enemy.cs并添加以下方法:

void FixedUpdate () {
  if(targetTransform != null) {
    this.transform.position = Vector3.MoveTowards(this.transform.position, targetTransform.transform.position, Time.deltaTime * moveSpeed);
  }
}

public void TakeDamage(int damage) {
  health -= damage;
  if(health <= 0) {
    Destroy(this.gameObject);
  }
}

public void Attack(Player player) {
  player.health -= this.damage;
  Destroy(this.gameObject);
}

你已经熟悉FixedUpdate()了,不同的是你使用的是MoveTowards()而不是Lerp()。 这是因为敌人应该始终以相同的速度移动,而不是在接近目标时轻松进入。 当敌人被射弹击中时,会调用TakeDamage();当敌人达到0健康时,它会自我毁灭。Attack()类似 - 它对玩家施加伤害然后敌人摧毁自己。

切换回Player.cs并在collidedWithEnemy()中,用以下内容替换Enemy attack code

enemy.Attack(this);

玩家将受到伤害,敌人将在此过程中自我毁灭。

切换回Unity。 将Enemy脚本附加到Enemy对象并在Inspector中,在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()

if(enemy) {
  collidedWithEnemy(enemy);
}

这样就没有null了。

后记

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容