不好意思,最近公司成员扩招,然后技术培训,项目事宜原因,因此这篇文章等到现在才出。
好了,不多说其它。
文章适合人群:对Unity基础组件有一些了解的,想知道怎么在项目中具体应用各种组件。
这篇文章以一个Asset Store上面的例子“Unity Projects Stealth”来讲解Unity的一些知识。所以可能你要对Unity一些概念有个了解。另外,这个例子"Unity Projects Stealth.unitypackage"我已经分享到http://pan.baidu.com/s/1gd6A0L1了,大家也可以去下载下来参照着文章看。
这篇项目会大概分成以下部分来讲(这个目录在后续文章编写中可能会根据需要稍作修改):
1、项目大致了解
2、碰撞器基础
3、控制角色
4、碰撞器及其使用
5、怪物AI
6、事件通知
7、渲染特效
OK。
在此之前先说下本文运行项目时的环境:
系统 :Window 7 X64
Unity:4.3.3f1
我们打开unity,新建项目,然后导入上面下载的"Unity Projects Stealth.unitypackage"。等导入完毕后就可以看到“Project”视图(如果你没打开,可以从菜单栏“Window”-“Project”打开)里面的结构如下:
里面每个目录大家都可以随便打开看看:
Animations | 一些角色动画 |
Animator | 动画控制器,实际里面啥都没有-_- |
Audio | 音频文件 |
Fonts | 字体 |
Gizmos | 在scene视图用于调试的一些图标 |
Materials | 材质 |
Models | 模型 |
Prefabs | 预设 |
Scenes | 场景文件,实际里面啥都没有-_- |
Scripts | 脚本,实际里面啥都没有-_- |
Shaders | 着色器 |
Textures | 一些贴图纹理 |
Done | 这里面才包含了上面缺少的动画控制器、场景文件和脚本〜· |
大家平常做项目也可以参考上面的做法,对不同的资源放不同目录进行归类整理,而且大家一看到这些文件夹就都知道里面有什么资源。可以减少很多沟通成本,也为自己查找资源带来便利。
好了,如果没什么问题,点击播放按钮,应该就能运行项目了。
使用键盘的WASD或上下左右箭头就可以控制人物走动了。屏幕左下方也有文本给出其它操作的按键。比如"Z"可以打开开关等。
一般情况下,你想让物体直接有体积,能产生碰撞,那么你需要给这个物体增加一个碰撞器“Collider”。在Unity中,你只需要选择一个对象,然后点击菜单栏的”Component“-“Physics”,里面就有各种各样的碰撞器,你根据自己模型选择一个比较接近的就OK了。Unity里面的碰撞器区分为两种,一种为静态的,另一种动态的。
静态的意思是这个碰撞器在游戏过程中不会发生位移、旋转、缩放。
动态的意思则是说这个碰撞器可能会在游戏过程中发生位移、旋转、缩放。动态碰撞器有两种:
一种是CharacterController。
另一种是普通的碰撞器+刚体组件。
第二种一定要增加刚体组件。不然可能会导致碰撞失效、性能开销增加。(比如Unity一个UI组件NGUI,它新版本的Panel会检测当前对象是否带了Rigibody,如果没有则自动增加一个。就是为了防止开发者做一些界面动画,忘记修改添加Rigibody,导致UI按钮点击失效。)
控制一个角色在场景中运动,最简单的做法是把一个对象拖到场景中,然后根据按键。设置这个对象transform的position值。这样对象就可以在场景中运动起来。
获取按键可以用Input.GetAxis方法。获取X和Z轴的按键分别可以使用:Input.GetAxis("Horizontal") 和 Input.GetAxis("Vertical")方法。“Horizontal”和“Vertical”其实是在“Edit”-“Project Settings”-“Input”里面配置的。
那么我们这个例子是使用什么方法来让玩家角色运动的呢?
先在“Hierarchy”视图(如果没有这个视图,可以在“Window”-“Hierarchy”打开它)中找到“char_ethan”对象,选中它。
我们先看看它的“Inspector”视图(如果没有可以在“Window”-“Inspector”打开它)。
注意这里除了有基本的Transform组件外,还有“Animator”,“Capsule Collider”,“Rigidbody”组件。这三个组件其实用于做可移动物体其实是黄金组合(当然也有用“Character Controller”的)。
还有“Audio Source”、“Audio Listener”两个分别是播放声音和监听声音的组件。
然后"Done Player Health (Script)","Done Player Inventory (Script)","Done Player Movement (Script)"分别是对角色做控制的脚本。下面我们简单说说每个组件的用途:
其实是Unity内置的一个动画控制器,原理是状态机。比如双击上面的“Animator”面板里面的“Controller”属性
就会出现状态机的编辑窗口
胶囊体碰撞器。比较简单,没什么好说的。
刚刚上面也说了,动态碰撞器的需要。
关于A、B、C这几个组件上面就只简单介绍到这里,这里不展开讨论。如果有需要,我后面会再单独写文章介绍。
下面开始看看控制代码部分。
这里有个“DonePlayerMovement(Script)”组件,从名字上看就知道应该是控制移动的(所以说命名很重要)。我们打开这个组件的源码,看看。
using UnityEngine; using System.Collections; public class DonePlayerMovement : MonoBehaviour { public AudioClip shoutingClip; // 玩家大喊的声音 public float turnSmoothing = 15f; // 用于玩家平滑转向的值 public float speedDampTime = 0.1f; // 用于控制从一个值变化到另一个的时间限制 private Animator anim; private DoneHashIDs hash; // 保存各种动画状态的hash void Awake () { anim = GetComponent<Animator>(); hash = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneHashIDs>(); // Set the weight of the shouting layer to 1. anim.SetLayerWeight(1, 1f); } void FixedUpdate () { // Cache the inputs. float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); bool sneak = Input.GetButton("Sneak"); MovementManagement(h, v, sneak); } void Update () { // Cache the attention attracting input. bool shout = Input.GetButtonDown("Attract"); // Set the animator shouting parameter. anim.SetBool(hash.shoutingBool, shout); AudioManagement(shout); } void MovementManagement (float horizontal, float vertical, bool sneaking) { // Set the sneaking parameter to the sneak input. anim.SetBool(hash.sneakingBool, sneaking); // If there is some axis input... if(horizontal != 0f || vertical != 0f) { // ... set the players rotation and set the speed parameter to 5.5f. Rotating(horizontal, vertical); anim.SetFloat(hash.speedFloat, 5.5f, speedDampTime, Time.deltaTime); } else // Otherwise set the speed parameter to 0. anim.SetFloat(hash.speedFloat, 0); } void Rotating (float horizontal, float vertical) { // Create a new vector of the horizontal and vertical inputs. Vector3 targetDirection = new Vector3(horizontal, 0f, vertical); // Create a rotation based on this new vector assuming that up is the global y axis. Quaternion targetRotation = Quaternion.LookRotation(targetDirection, Vector3.up); // Create a rotation that is an increment closer to the target rotation from the player's rotation. Quaternion newRotation = Quaternion.Lerp(rigidbody.rotation, targetRotation, turnSmoothing * Time.deltaTime); // Change the players rotation to this new rotation. rigidbody.MoveRotation(newRotation); } void AudioManagement (bool shout) { // If the player is currently in the run state... if(anim.GetCurrentAnimatorStateInfo(0).nameHash == hash.locomotionState) { // ... and if the footsteps are not playing... if(!audio.isPlaying) // ... play them. audio.Play(); } else // Otherwise stop the footsteps. audio.Stop(); // If the shout input has been pressed... if(shout) // ... play the shouting clip where we are. AudioSource.PlayClipAtPoint(shoutingClip, transform.position); } }
在Awake的时候,先找到脚本需要用到的Animator组件和DoneHashIds组件,然后缓存它们(在后面用到就不需要频繁查找了,节省CPU)。
在FixedUpdate的时候,获取玩家按下的移动键,并处理移动、角色朝向问题。
在Update的时候,获取玩家是否按下“Attract”键,并确定是否播放动画以及声音。
OK,我们主要来看看MovementManagement这个方法:
void MovementManagement (float horizontal, float vertical, bool sneaking) { // Set the sneaking parameter to the sneak input. anim.SetBool(hash.sneakingBool, sneaking); // If there is some axis input... if(horizontal != 0f || vertical != 0f) { // ... set the players rotation and set the speed parameter to 5.5f. Rotating(horizontal, vertical); anim.SetFloat(hash.speedFloat, 5.5f, speedDampTime, Time.deltaTime); } else // Otherwise set the speed parameter to 0. anim.SetFloat(hash.speedFloat, 0); }我们可以发现,除了Rotating方法,基本都是对anim做一些状态设置的方法。那么Rotating方法里面是什么呢?
void Rotating (float horizontal, float vertical) { // 创建一个Vector3用于保存输入的水平位移方向 Vector3 targetDirection = new Vector3(horizontal, 0f, vertical); // 根据上面的方向计算这个方向指向的角度 Quaternion targetRotation = Quaternion.LookRotation(targetDirection, Vector3.up); // 创建一个从玩家当前方向旋转到目标方向的旋转增量 Quaternion newRotation = Quaternion.Lerp(rigidbody.rotation, targetRotation, turnSmoothing * Time.deltaTime); // 修改玩家的方向 rigidbody.MoveRotation(newRotation); }
上面代码其实看出,他也没有实现玩家的位置移动。那是在其它组件里面实现的还是什么其它方式处理的呢?且听下回分解。
---------------------------------------------------------------------------------------------------------------------
好的,这次距离上次时间有点久,本来说一周更新一次,但实在没办法。在公司客户端、服务端都要搞,而且战斗系统重新重构优化,也花了不少时间。望见谅。
我们接上次的话题:代码没有实现玩家的位置移动的代码,但是实际运行的时候却可以看到角色会移动。这是为什么?
我们一起看到“Hierarchy”视图中的“char_ethan”。注意里面的Animator组件(它勾选了“Apply Root Motion”):
我们看到官方文档对于这个选项是这么介绍的:
Root motion is the effect where an object's entire mesh moves away from its starting point but that motion is created by the animation itself rather than by changing the Transform position.
这里大致的意思其实就是:这个选项会在由动画产生移动时,使得物体的mesh会偏离起点。
那也就是物体的移动是做在动画中的,然后勾选Animator的ApplyRootMotion选项来实现的。
关于角色控制中anim.SetFloat的方法的。我们在这里暂时不讲。这个涉及到动画混合,以后会专门讲。
那么角色的移动、旋转就都讲了。接下来我们看看碰撞器。
前面我们说了碰撞器分静态和动态两种。
那么角色“char_ethan”身上的肯定就是动态碰撞器了,注意到“char_ethan”对象有个“Capsule Collider”组件。这就是一个胶囊体碰撞器组件。然后角色还有个“rigidbody”组件,这个是刚体组件。也就是采用前面说的普通碰撞器+刚体组件。
关于各种内置碰撞器的知识。大家不了解的可以看看官网http://docs.unity3d.com/Manual/Physics3DReference.html(我不会告诉你我大部分U3D的知识都在官网学习的。)
这里只要知道碰撞器一个重要的选项“Is Trigger”
只要两个相互碰撞的碰撞器,有任何一个勾选了IsTrigger,那么就会触发这两个碰撞器上面的OnTriggerEnter方法。
如果两个相互碰撞的碰撞器,都没勾选IsTrigger,那么会触发这两个碰撞器上面的OnCollisionEnter,OnCollisionStay,OnCollisionExit方法。
只有在不勾选IsTrigger的时候,才能使用Physics.Raycast进行射线检测。
所以当你的碰撞方法没有调用的时候,请确保IsTrigger和方法是符合的。
注意:这个游戏中玩家走路的时候不会穿墙,因为墙的碰撞器和玩家身上的碰撞器都没有勾选IsTrigger。
在“Hierarchy”视图中找到三个怪物“char_robotGuard_001”、“char_robotGuard_002”、“char_robotGuard_003”,这三个怪物上面的组件其实都是一样的。我们来看看“char_robotGuard_001”就可以了。看看怪物是怎么巡逻、然后发现敌人进行追踪的。
怪物身上有如下组件:
两个碰撞器?为什么这里要用两个碰撞器?我们先看看两个碰撞器的属性:
一个是胶囊体,没有勾选IsTrigger。这个应该是配合走路,为了不让角色穿过建筑物的。
第二个是球体碰撞器,勾选了IsTrigger。干嘛用的,别急,我们还是先来看那4个脚本吧。
二话不说我们先上代码
using UnityEngine; using System.Collections; public class DoneEnemySight : MonoBehaviour { public float fieldOfViewAngle = 110f; // Number of degrees, centred on forward, for the enemy see. public bool playerInSight; // Whether or not the player is currently sighted. public Vector3 personalLastSighting; // Last place this enemy spotted the player. private NavMeshAgent nav; // Reference to the NavMeshAgent component. private SphereCollider col; // Reference to the sphere collider trigger component. private Animator anim; // Reference to the Animator. private DoneLastPlayerSighting lastPlayerSighting; // Reference to last global sighting of the player. private GameObject player; // Reference to the player. private Animator playerAnim; // Reference to the player's animator component. private DonePlayerHealth playerHealth; // Reference to the player's health script. private DoneHashIDs hash; // Reference to the HashIDs. private Vector3 previousSighting; // Where the player was sighted last frame. void Awake () { // Setting up the references. nav = GetComponent<NavMeshAgent>(); col = GetComponent<SphereCollider>(); anim = GetComponent<Animator>(); lastPlayerSighting = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneLastPlayerSighting>(); player = GameObject.FindGameObjectWithTag(DoneTags.player); playerAnim = player.GetComponent<Animator>(); playerHealth = player.GetComponent<DonePlayerHealth>(); hash = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneHashIDs>(); // Set the personal sighting and the previous sighting to the reset position. personalLastSighting = lastPlayerSighting.resetPosition; previousSighting = lastPlayerSighting.resetPosition; } void Update () { // If the last global sighting of the player has changed... if(lastPlayerSighting.position != previousSighting) // ... then update the personal sighting to be the same as the global sighting. personalLastSighting = lastPlayerSighting.position; // Set the previous sighting to the be the sighting from this frame. previousSighting = lastPlayerSighting.position; // If the player is alive... if(playerHealth.health > 0f) // ... set the animator parameter to whether the player is in sight or not. anim.SetBool(hash.playerInSightBool, playerInSight); else // ... set the animator parameter to false. anim.SetBool(hash.playerInSightBool, false); } void OnTriggerStay (Collider other) { // If the player has entered the trigger sphere... if(other.gameObject == player) { // By default the player is not in sight. playerInSight = false; // Create a vector from the enemy to the player and store the angle between it and forward. Vector3 direction = other.transform.position - transform.position; float angle = Vector3.Angle(direction, transform.forward); // If the angle between forward and where the player is, is less than half the angle of view... if(angle < fieldOfViewAngle * 0.5f) { RaycastHit hit; // ... and if a raycast towards the player hits something... if(Physics.Raycast(transform.position + transform.up, direction.normalized, out hit, col.radius)) { // ... and if the raycast hits the player... if(hit.collider.gameObject == player) { // ... the player is in sight. playerInSight = true; // Set the last global sighting is the players current position. lastPlayerSighting.position = player.transform.position; } } } // Store the name hashes of the current states. int playerLayerZeroStateHash = playerAnim.GetCurrentAnimatorStateInfo(0).nameHash; int playerLayerOneStateHash = playerAnim.GetCurrentAnimatorStateInfo(1).nameHash; // If the player is running or is attracting attention... if(playerLayerZeroStateHash == hash.locomotionState || playerLayerOneStateHash == hash.shoutState) { // ... and if the player is within hearing range... if(CalculatePathLength(player.transform.position) <= col.radius) // ... set the last personal sighting of the player to the player's current position. personalLastSighting = player.transform.position; } } } void OnTriggerExit (Collider other) { // If the player leaves the trigger zone... if(other.gameObject == player) // ... the player is not in sight. playerInSight = false; } float CalculatePathLength (Vector3 targetPosition) { // Create a path and set it based on a target position. NavMeshPath path = new NavMeshPath(); if(nav.enabled) nav.CalculatePath(targetPosition, path); // Create an array of points which is the length of the number of corners in the path + 2. Vector3 [] allWayPoints = new Vector3[path.corners.Length + 2]; // The first point is the enemy's position. allWayPoints[0] = transform.position; // The last point is the target position. allWayPoints[allWayPoints.Length - 1] = targetPosition; // The points inbetween are the corners of the path. for(int i = 0; i < path.corners.Length; i++) { allWayPoints[i + 1] = path.corners[i]; } // Create a float to store the path length that is by default 0. float pathLength = 0; // Increment the path length by an amount equal to the distance between each waypoint and the next. for(int i = 0; i < allWayPoints.Length - 1; i++) { pathLength += Vector3.Distance(allWayPoints[i], allWayPoints[i + 1]); } return pathLength; } }
Awake | 主要是获取一些组件,在后面的逻辑中可以使用。 |
Update | 检查玩家最后一次被发现的位置是否一致,不是则同步位置。 把playerInSight(是否发现玩家)这个状态设置到Animator的状态机里面。 |
OnTriggerStay | 没错,前面说到的第二个碰撞器正是这里要用到的。第二个碰撞器勾选了IsTrigger,就是为了触发这个方法。 当玩家进入这个碰撞器(也就是怪物的视野)触发。 检查玩家是否在怪物前方,而且玩家和怪物中间没有遮挡物。那么设置playerInSight为true。 |
OnTriggerExit | 当玩家离开怪物的视野(碰撞器)。那么设置playerInSight为false。 |
CalculatePathLength | 计算怪物和玩家当前的距离。 |
我们接着看DoneEnemyAI这个组件
还是上代码:
using UnityEngine; using System.Collections; public class DoneEnemyAI : MonoBehaviour { public float patrolSpeed = 2f; // The nav mesh agent's speed when patrolling. public float chaseSpeed = 5f; // The nav mesh agent's speed when chasing. public float chaseWaitTime = 5f; // The amount of time to wait when the last sighting is reached. public float patrolWaitTime = 1f; // The amount of time to wait when the patrol way point is reached. public Transform[] patrolWayPoints; // An array of transforms for the patrol route. private DoneEnemySight enemySight; // Reference to the EnemySight script. private NavMeshAgent nav; // Reference to the nav mesh agent. private Transform player; // Reference to the player's transform. private DonePlayerHealth playerHealth; // Reference to the PlayerHealth script. private DoneLastPlayerSighting lastPlayerSighting; // Reference to the last global sighting of the player. private float chaseTimer; // A timer for the chaseWaitTime. private float patrolTimer; // A timer for the patrolWaitTime. private int wayPointIndex; // A counter for the way point array. void Awake () { // Setting up the references. enemySight = GetComponent<DoneEnemySight>(); nav = GetComponent<NavMeshAgent>(); player = GameObject.FindGameObjectWithTag(DoneTags.player).transform; playerHealth = player.GetComponent<DonePlayerHealth>(); lastPlayerSighting = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneLastPlayerSighting>(); } void Update () { // If the player is in sight and is alive... if(enemySight.playerInSight && playerHealth.health > 0f) // ... shoot. Shooting(); // If the player has been sighted and isn't dead... else if(enemySight.personalLastSighting != lastPlayerSighting.resetPosition && playerHealth.health > 0f) // ... chase. Chasing(); // Otherwise... else // ... patrol. Patrolling(); } void Shooting () { // Stop the enemy where it is. nav.Stop(); } void Chasing () { // Create a vector from the enemy to the last sighting of the player. Vector3 sightingDeltaPos = enemySight.personalLastSighting - transform.position; // If the the last personal sighting of the player is not close... if(sightingDeltaPos.sqrMagnitude > 4f) // ... set the destination for the NavMeshAgent to the last personal sighting of the player. nav.destination = enemySight.personalLastSighting; // Set the appropriate speed for the NavMeshAgent. nav.speed = chaseSpeed; // If near the last personal sighting... if(nav.remainingDistance < nav.stoppingDistance) { // ... increment the timer. chaseTimer += Time.deltaTime; // If the timer exceeds the wait time... if(chaseTimer >= chaseWaitTime) { // ... reset last global sighting, the last personal sighting and the timer. lastPlayerSighting.position = lastPlayerSighting.resetPosition; enemySight.personalLastSighting = lastPlayerSighting.resetPosition; chaseTimer = 0f; } } else // If not near the last sighting personal sighting of the player, reset the timer. chaseTimer = 0f; } void Patrolling () { // Set an appropriate speed for the NavMeshAgent. nav.speed = patrolSpeed; // If near the next waypoint or there is no destination... if(nav.destination == lastPlayerSighting.resetPosition || nav.remainingDistance < nav.stoppingDistance) { // ... increment the timer. patrolTimer += Time.deltaTime; // If the timer exceeds the wait time... if(patrolTimer >= patrolWaitTime) { // ... increment the wayPointIndex. if(wayPointIndex == patrolWayPoints.Length - 1) wayPointIndex = 0; else wayPointIndex++; // Reset the timer. patrolTimer = 0; } } else // If not near a destination, reset the timer. patrolTimer = 0; // Set the destination to the patrolWayPoint. nav.destination = patrolWayPoints[wayPointIndex].position; } }这个组件主要是:如果判断玩家在视野中,则对玩家进行射击。如果玩家最后一次被发现的点不是默认点,则进行追击。否则就进行巡逻。
追击、巡逻,主要都是对NavMeshAgent进行设置来实现控制怪物的移动。关于NavMeshAgent我们以后再专门来讲解。
而射击调用的Shooting方法里面只有一行代码:nav.Stop。那么射击具体在哪里实现的?接着看DoneEnemyShooting组件。
你知道我肯定会说,看代码。。。没错:
using UnityEngine; using System.Collections; public class DoneEnemyShooting : MonoBehaviour { public float maximumDamage = 120f; // The maximum potential damage per shot. public float minimumDamage = 45f; // The minimum potential damage per shot. public AudioClip shotClip; // An audio clip to play when a shot happens. public float flashIntensity = 3f; // The intensity of the light when the shot happens. public float fadeSpeed = 10f; // How fast the light will fade after the shot. private Animator anim; // Reference to the animator. private DoneHashIDs hash; // Reference to the HashIDs script. private LineRenderer laserShotLine; // Reference to the laser shot line renderer. private Light laserShotLight; // Reference to the laser shot light. private SphereCollider col; // Reference to the sphere collider. private Transform player; // Reference to the player's transform. private DonePlayerHealth playerHealth; // Reference to the player's health. private bool shooting; // A bool to say whether or not the enemy is currently shooting. private float scaledDamage; // Amount of damage that is scaled by the distance from the player. void Awake () { // Setting up the references. anim = GetComponent<Animator>(); laserShotLine = GetComponentInChildren<LineRenderer>(); laserShotLight = laserShotLine.gameObject.light; col = GetComponent<SphereCollider>(); player = GameObject.FindGameObjectWithTag(DoneTags.player).transform; playerHealth = player.gameObject.GetComponent<DonePlayerHealth>(); hash = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneHashIDs>(); // The line renderer and light are off to start. laserShotLine.enabled = false; laserShotLight.intensity = 0f; // The scaledDamage is the difference between the maximum and the minimum damage. scaledDamage = maximumDamage - minimumDamage; } void Update () { // Cache the current value of the shot curve. float shot = anim.GetFloat(hash.shotFloat); // If the shot curve is peaking and the enemy is not currently shooting... if(shot > 0.5f && !shooting) // ... shoot Shoot(); // If the shot curve is no longer peaking... if(shot < 0.5f) { // ... the enemy is no longer shooting and disable the line renderer. shooting = false; laserShotLine.enabled = false; } // Fade the light out. laserShotLight.intensity = Mathf.Lerp(laserShotLight.intensity, 0f, fadeSpeed * Time.deltaTime); } void OnAnimatorIK (int layerIndex) { // Cache the current value of the AimWeight curve. float aimWeight = anim.GetFloat(hash.aimWeightFloat); // Set the IK position of the right hand to the player's centre. anim.SetIKPosition(AvatarIKGoal.RightHand, player.position + Vector3.up * 1.5f); // Set the weight of the IK compared to animation to that of the curve. anim.SetIKPositionWeight(AvatarIKGoal.RightHand, aimWeight); } void Shoot () { // The enemy is shooting. shooting = true; // The fractional distance from the player, 1 is next to the player, 0 is the player is at the extent of the sphere collider. float fractionalDistance = (col.radius - Vector3.Distance(transform.position, player.position)) / col.radius; // The damage is the scaled damage, scaled by the fractional distance, plus the minimum damage. float damage = scaledDamage * fractionalDistance + minimumDamage; // The player takes damage. playerHealth.TakeDamage(damage); // Display the shot effects. ShotEffects(); } void ShotEffects () { // Set the initial position of the line renderer to the position of the muzzle. laserShotLine.SetPosition(0, laserShotLine.transform.position); // Set the end position of the player's centre of mass. laserShotLine.SetPosition(1, player.position + Vector3.up * 1.5f); // Turn on the line renderer. laserShotLine.enabled = true; // Make the light flash. laserShotLight.intensity = flashIntensity; // Play the gun shot clip at the position of the muzzle flare. AudioSource.PlayClipAtPoint(shotClip, laserShotLight.transform.position); } }Update的时候可以发现它检查动画的shotFloat属性。然后判断是否应该射击或取消射击。
而射击的激光,这里可以看到是采用LineRenderer实现的。
以上关于怪物的AI我们就讲完了。但这只是单个怪物的AI。实际这个游戏里面还有用于监控玩家的摄像头、还有红外线墙检测玩家是否通过。但是原理都一样。都是通过监听碰撞器的OnTriggerStay来设置玩家被发现的全局位置。也就是DoneLastPlayerSighting。
个人关于这个demo代码设计的看法:
我个人觉得这种做法不好。大家都自己去获取DoneLastPlayerSighting,然后自己检查里面的状态。然后还可以设置里面的状态。这样会导致程序变量跟踪困难。
如果是我的话,我会把DoneLastPlayerSighting做成是一个消息中心。然后每个敌人订阅这个消息中心的消息(即玩家被发现的位置发生变化的消息),然后由消息中心通知所有订阅的对象。然后订阅的对象也可以提交自己发现的目标给消息中心,如果消息中心确认通过,再通知已经订阅的其它对象。这样做起来代码会更清晰,更容易维护。
下一篇我会专门来讲关于这部分代码的一些优化。
你可以通过点击Unity的菜单栏“Edit”-“Render Settings”。这时候在右边的Inspector面板可以通过勾选“fog”选项来开启雾效。“Fog Color”可以调整雾的颜色。
在“Hiererchy”中找到监视器物体“prop_cctvCam_001”,你会发现它下面有个子物体“cam_frustum_collision”,它上面有个Light组件,Light上的Cookie属性就是产生亮点的贴图。
其实就是用了一个"Self-Illumin/Diffuse"的shader,这是一个自发光shader,你可以尝试一下,在相同的灯光条件下,采用自发光shader的物体总会比Diffuse的亮。