从Hierarchy视图中可以看见,Enemies对象下面挂有很多子对象,很多都是Prefab。而点击这些子对象,其实发现它们的很多地方有很大的相同之处,就拿SimpleBuzzers来看,里面的怪物KamikazeBuzzer都是相同的怪物Prefab,随便点击一个,都可以看见包含KamikazeMovementMotor.js脚本,BuzzerKamikazeControllerAndAi.js脚本,Health.js脚本,和DestroyObject.js脚本,Sphere Collider,Rigidbody,Audio Source等。以下分析就以SimpleBuzzers来进行的:
怪物的激活:
每个SimpleBuzzer对象点击后,可以在Inspector中看到Transform、EnemyArea.js脚本及Box Collider。而EnemyArea.js就是用于激活怪物的脚本。
在编辑器模式下,当我们点击SimpleBuzzer*时,可以看见Scene视图中会有相应的长方体框显示,这个框就是Box Collider的边界,标记了盒状碰撞器的范围,实际上也是怪物活动范围(或者说看守范围)。
#pragma strict
#pragma downcast
import System.Collections.Generic;
public var affected : List.<GameObject> = new List.<GameObject> ();//主要记录受影响的对象,即在此范围有事件发生时会采取动作的对象的集合
ActivateAffected (false);//初始化时将所有对象标记为未激活状态
function OnTriggerEnter (other : Collider) {//当有其它碰撞器碰撞到该触发器上时
if (other.tag == "Player")
ActivateAffected (true);//如果碰撞器是人物所带碰撞器时,将affected集合中所有的对象激活(即人物入侵,怪物激活采取动作)
}
function OnTriggerExit (other : Collider) {//当有其它碰撞器离开该触发器时
if (other.tag == "Player")
ActivateAffected (false);//如果碰撞器是人物所带碰撞器时,将affected集合中所有的对象设为非激活状态(即人物离开防范领地,不需要警戒状态,该干嘛干嘛去)
}
function ActivateAffected (state : boolean) {
for (var go : GameObject in affected) {//将affected集合中的所有对象设置好状态
if (go == null)
continue;
go.SetActive (state);//设置状态
yield;
}
for (var tr : Transform in transform) {
tr.gameObject.SetActive (state);
yield;
}
}
当物理引擎在每固定时间帧去检测游戏中所有碰撞器、触发器等是否发生碰撞,如果发生碰撞就会将相应的Trigger消息或Collision消息发送给受到碰撞的两个物体,那么这两个物体上挂载的脚本会处理相应的消息事件。就像上面脚本中写的,是消息处理事件,是触发之后我们决定采取什么行动都写在消息事件处理函数中。人物入侵,所有怪物处于激活状态,怪物身上挂着的脚本就会开始执行了。
怪物的攻击:
会根据人物的位置,设置怪物的移动目标movementTarget始终为人物所在位置;并根据与人物之间的距离判断是否在威胁范围内;电弧定时器到时并在威胁范围内并追到人物则发动攻击,对目标生命值造成伤害;施放电弧展示及音效;然后随机重置电弧发射的定时器。
#pragma strict
public var motor : MovementMotor; //MovementMotor对象,保存移动方向、朝向、移动目标
public var electricArc : LineRenderer; //线渲染器,用于怪物发射电弧的绘制
public var zapSound : AudioClip; //声频剪辑,用于怪物攻击产生电弧时伴随的声音
public var damageAmount : float = 5.0f;//受伤害的大小
private var player : Transform; //人物的Transform
private var character : Transform; //怪物的Transform
private var spawnPos : Vector3; //怪物的产生点
private var startTime : float; //启动时间
private var threatRange : boolean = false;//怪物是否受到威胁,即人物是否在怪物的攻击范围内
private var direction : Vector3; //存储从怪物到人物的距离向量
private var rechargeTimer : float = 1.0f;//电弧显示定时器
private var audioSource : AudioSource; //声源
private var zapNoise : Vector3 = Vector3.zero;//用于设置怪物对人物伤害的小随机变量
function Awake () {
character = motor.transform; //怪物Transform赋值
player = GameObject.FindWithTag ("Player").transform;//人物Transform赋值,通过FindWithTag来获取
spawnPos = character.position; //怪物的位置
audioSource = GetComponent.<AudioSource> ();//声源赋值
}
function Start () {
startTime = Time.time;
motor.movementTarget = spawnPos; //怪物的移动目标为怪物产生点
threatRange = false;//攻击范围没有受到侵犯,即人物不在怪物的攻击范围内
}
function Update () {
motor.movementTarget = player.position; //怪物的移动目标始终为人物的位置
direction = (player.position - character.position);//从怪物到人物的距离向量
threatRange = false;//未受到威胁
if (direction.magnitude < 2.0f) {//假如怪物和人物离得太近了
threatRange = true;//怪物受到威胁
motor.movementTarget = Vector3.zero;//不用移动了,原地呆着
}
rechargeTimer -= Time.deltaTime;//电弧发射定时器减去上一帧花的时间
//假如电弧显示定时器到时了,并且怪物受到威胁,并且怪物的forward方向(前方)与怪物和人物的距离向量之间的角度比较小
if (rechargeTimer < 0.0f && threatRange && Vector3.Dot (character.forward, direction) > 0.8f) {
zapNoise = Vector3 (Random.Range (-1.0f, 1.0f), 0.0f, Random.Range(-1.0f, 1.0f)) * 0.5f;//使每次人物受到伤害有些小随机
var targetHealth : Health = player.GetComponent.<Health> ();//人物生命值
if (targetHealth) {
var playerDir : Vector3 = player.position - character.position;//怪物到人物的距离向量
var playerDist : float = playerDir.magnitude;//怪物到人物的距离
playerDir /= playerDist;//归一化攻击向量
targetHealth.OnDamage (damageAmount / (1.0f + zapNoise.magnitude), -playerDir);//人物受到伤害
}
DoElectricArc(); //施放电弧显示
rechargeTimer = Random.Range (1.0f, 2.0f);//随机重置电弧发射的定时器
}
}
function DoElectricArc () {
if (electricArc.enabled)
return;
//播放声音
audioSource.clip = zapSound;
audioSource.Play ();
//设置怪物电弧为enabled
electricArc.enabled = true;
zapNoise = transform.rotation * zapNoise;//使每次电到人物的位置不同
//显示电弧,并绘制纹理(绘制多条连续线段来产生电弧效果)
var stopTime : float = Time.time + 0.2;//电弧从现在开始持续0.2s
while (Time.time < stopTime) {//如果没有电弧显示结束时间
electricArc.SetPosition (0, electricArc.transform.position);//设置电弧一个端点
electricArc.SetPosition (1, player.position + zapNoise);//设置电弧的另一个端点
electricArc.sharedMaterial.mainTextureOffset.x = Random.value;//共享纹理设置
yield;
}
//隐藏电弧
electricArc.enabled = false;
}
- 攻击特效主要利用DoElectricArc函数来表达攻击方式,函数里播放了声音效果,而且通过LineRenderer构造多条连续线段,来制造闪电弧效果。
怪物的动作逻辑:
怪物的动作和人物动作控制逻辑差不多,都是继承类MovementMotor,并通过一些参数及movementTarget 来改变怪物的运动的。
#pragma strict
class KamikazeMovementMotor extends MovementMotor {
public var flyingSpeed : float = 5.0;//怪物向前飞的速度
public var zigZagness : float = 3.0f;//怪物移动影响因子
public var zigZagSpeed : float = 2.5f;//怪物之字形移动速度
public var oriantationMultiplier : float = 2.5f;//怪物方向旋转影响因子
public var backtrackIntensity : float = 0.5f;//怪物回溯强度大小
private var smoothedDirection : Vector3 = Vector3.zero;//怪物转动方向平滑因子
function FixedUpdate () {
var dir : Vector3 = movementTarget - transform.position;//移动方向设置为从自身位置到目标位置
var zigzag : Vector3 = transform.right * (Mathf.PingPong (Time.time * zigZagSpeed, 2.0) - 1.0) * zigZagness;//怪物之字形移动速度
dir.Normalize ();//移动方向归一化
smoothedDirection = Vector3.Slerp (smoothedDirection, dir, Time.deltaTime * 3.0f);//获取平滑的移动方向,防止变换突兀
var orientationSpeed = 1.0f;//旋转速度设置
var deltaVelocity : Vector3 = (smoothedDirection * flyingSpeed + zigzag) - rigidbody.velocity;//速度差值
if (Vector3.Dot (dir, transform.forward) > 0.8f)//移动方向和怪物现在的正前方夹角比较小的情况(即怪物只需稍微移动即可)
rigidbody.AddForce (deltaVelocity, ForceMode.Force);//对怪物身上刚体施加外力作用
else {//否则让怪物向相反方向移动
rigidbody.AddForce (-deltaVelocity * backtrackIntensity, ForceMode.Force);//反速度方向的力,游戏中可以观察到怪物有的时候前进攻击,有的时候旋转,有的时候会后退伴随旋转
orientationSpeed = oriantationMultiplier;
}
//使怪物旋转到目标方向
var faceDir : Vector3 = smoothedDirection;
if (faceDir == Vector3.zero) {
rigidbody.angularVelocity = Vector3.zero;//不旋转的时候,设置刚体转动角速度为zero
}
else {
var rotationAngle : float = AngleAroundAxis (transform.forward, faceDir, Vector3.up);//世界坐标系中,将怪物的transform中存储的前方,旋转到要面朝方向所需转动的角度
rigidbody.angularVelocity = (Vector3.up * rotationAngle * 0.2f * orientationSpeed);//设置刚体角速度让其转起来
}
}
//方向dirA绕轴axis旋转到方向dirB所需转动的角度
static function AngleAroundAxis (dirA : Vector3, dirB : Vector3, axis : Vector3) {
//dirA和dirB在与轴垂直的平面上的投影,这样以便得到两者直接的角度
dirA = dirA - Vector3.Project (dirA, axis);
dirB = dirB - Vector3.Project (dirB, axis);
//dirA和dirB之间角度的正值
var angle : float = Vector3.Angle (dirA, dirB);
//根据dirA旋转到dirB叉乘正方向与axis方向,得出旋转角度的正负
return angle * (Vector3.Dot (axis, Vector3.Cross (dirA, dirB)) < 0 ? -1 : 1);
}
function OnCollisionEnter (collisionInfo : Collision) {//产生碰撞无动作
}
}
- rigidbody.AddForce (deltaVelocity, ForceMode.Force);中为刚体施力的函数跟人物的不一样,人物利用加速模式,而对怪物利用的是考虑质量的持续的力,在每个FixedUpdate调用中持续一段时间。这种模式取决于刚体的质量,这样的话对于推或扭转更大质量的物体就需要更大的力。
- 为什么物理因素作用下的运动变换都是在FixedUpdate函数中定义的,而没有在Update函数中定义?由于机器不同其帧速率不同,会使每秒调用Update函数次数也会不同,即使在同一台机器,不同秒帧速率也会因为场景需要渲染的三角面数量不同,而被调用次数不同,帧的间隔时间不一定。Update函数会使用该帧与上一帧的时间间隔,FixedUpdate函数会使用固定时间间隔,这样两者的时间差会导致每一帧出现误差,最后模拟出来的物理现象与理论不符合。物理引擎对刚体的各种模拟都是以FixedUpdate函数的时间间隔来计算的,使用Update函数会出错。