研究一下Unity3d自带的AngryBots项目,了解基本的游戏运行机制:
1. 人物的动作控制逻辑
 ***Player对象***
 [外形]
 Player对象里有一个对象具有Skinned Mesh Renderer组件,该组件使用的Mesh名为main_player_lorez。
 类似的还有表达武器的,名为main_weapon001的GameObject。[操作]: (InputManager)
 **移动**
 定义:
 移动在InputManager里添加了2种操作方式:
 水平移动,名为 Horizontal
 垂直移动,名为 Vertical
 并设置了一些属性,比如对应的按键,加速度,类型等等。
 在脚本(PlayerMoveController.js)里,通过Input.GetAxis("Horizontal") 和 Input.GetAxis("Vertical")获得玩家的按键状态转化成的运动方向。
 并储存在MovementMotor.js脚本定义的movementDirection变量里。实现:
 Player添加了RigidBody组件,该组件提供了按物理规律改变GameObject的Transform的能力。
 在FreeMovementMotor.js脚本里,定义了一些参数,用于和movementDirection一起,计算出作用于RigidBody对象上的力(Force)。角色就开始向指定方向移动了。**面向(facingDirection)**
 直接使用Input.mousePosition作为屏幕坐标,用角色所在位置定义一个平面,求得射线焦点,将该角色所在位置到该点的方向作为面向。
 并储存在MovementMotor.js脚本定义的facingDirection变量里。[动作播放]:Player Animation(Script)(PlayerAnimation.js)
 var moveAnimations : MoveAnimation[]; 因为是public变量,所以可以在inspector中直接修改,
 例子中定义了6个动作 run_forward/run_backward/run_right/run_left 和 idle/turn 。
 由于这个例子里角色的动作定义了6个clip,和上述6个动作名称一一对应。动作的播放不是在转向发生,或是ASWD按下时发生的。
 该脚本对比Player的Transform在2帧内的变化,根据面向、移动方向,计算出具体播放哪个动作。
 同时,有动作混合逻辑,使得动作的切换是有过程并且平滑的。
 上半身转动到一定角度,下半身也会调整,这个也是逻辑做的功能。2. 从射击到命中的整个处理流程,射击特效的制作原理
 [创建子弹]
 Cache对象
 ObjectCache类
 var prefab : GameObject;
 var cacheSize : int = 10;
 Spawner.js
 var caches : ObjectCache[];
 function Awake () {
 caches[i].Initialize ();
 }
 static function Spawn(...);
 static function Destroy(...);有一个对象cache池,即为objectCache对象的实例,
 该对象初始化固定数量的对象实例,并顺序的提供对象实例。
 Spawner对象按Prefab类型将多个ObjectCache对象组织起来,
 并通过Spawn 和 Destroy 函数提供统一的接口来创建和销毁各种对象实例--例如子弹,导弹。[发射子弹的时机]
 WeaponSlot
 TriggerOnMouseOrJoystick.js
 public var mouseDownSignals : SignalSender;
 public var mouseUpSignals : SignalSender;
 SignalSender.js
 public function SendSignals (sender : MonoBehaviour)
 public var receivers : ReceiverItem[];
 AutoFire.js
 function Update ()
 if (firing) {
 if (Time.time > lastFireTime + 1 / frequency) {
 var go : GameObject = Spawner.Spawn (bulletPrefab, spawnPoint.position, spawnPoint.rotation * coneRandomRotation) as GameObject;WeaponSlot(GameObject)对象有一个脚本组件 , 名为TriggerOnMouseOrJoystick
 该脚本的update方法通过Input.GetMouseButtonDown (0) 来监测鼠标左键的按下状态,同时使用SignalSender对象将事件Fire出去。
 SignalSender本质上来说是一个发布订阅模式,EventSource通过声明SignalSender变量,
 来声明会发起的事件(event name),并在必要的时机,调用SignalSender.SendSignals(this)来fire事件。
 事件的接收方由SignalSender的receivers变量给出。
 因为它是个全局变量,所以可以在inspector里设置。
 客户方的处理逻辑和事件源就是通过这样的方式关联起来的。
 事件的名称也是通过inspector来设置的。SendSignals方法接受的参数为MonoBehaviour类型,因此可以通过这个事件机制,在不同的脚本中调用不同的功能。
 由于GameObject的SendMessage的实现原理,只需要保证事件的接收方包含与事件名称相同的函数,即会被自动调用。
 (疑惑:这种方式是不带参数的,如果需要对Event做额外的参数传递怎么办呢?能想到的是在一个公共的地方做数据交换)
 通过SignalSender,武器的逻辑状态--"开火"--已经被逻辑识别了,例子将结果保存在AutoFire脚本的firing变量中。开火后,在AutoFire里,激活了子弹的实例对象。
 [开火的特效]
 WeaponSlot
 AutoFire.js
 muzzleFlashFront.active = true;
 audio.Play ();通过SignalSender,武器的逻辑状态--"开火"--已经被逻辑识别了,
 例子将结果保存在AutoFire脚本的firing变量中。同一时刻,也播放了一些开火的特效:
 *武器开火的音效,这只是调用AudioSource组件。
 *武器枪口的火花,muzzleFlashFront对象,在inspector中指定为一个GameObject。
 其中包含一些Mesh和一个Light,以及一个将Mesh旋转和缩放以达到比较酷的喷射火光的脚本。
 *人物的射击动作--通过另一组监听实现的,不在AutoFire脚本中触发。[命中时的事情]
 PerFrameRaycast.js
 private var hitInfo : RaycastHit;
 AutoFire.js(命中判定)
 var hitInfo : RaycastHit = raycast.GetHitInfo ();
 AutoFire.js(击退敌人)
 var force : Vector3 = transform.forward * (forcePerSecond / frequency);
 hitInfo.rigidbody.AddForceAtPosition (force, hitInfo.point, ForceMode.Impulse);
 AutoFire.js(播放击中的音效)
 var sound : AudioClip = MaterialImpactManager.GetBulletHitSound (hitInfo.collider.sharedMaterial);
 AudioSource.PlayClipAtPoint (sound, hitInfo.point, hitSoundVolume);游戏中实现的命中,和子弹飞行无关,是通过PerFrameRaycast.js脚本提供的射线查询结果来做的命中判定。
 PerFrameRaycast每帧做一次射线查询,将得到的结果保存在hitInfo中。
 AutoFire在update的时候,检查是否命中了对象。
 如果命中了对象,计算各种伤害,并调用Health脚本组件的相关方法。(Health相关的事情稍后详细描述)[子弹的飞行]
 子弹是一个名为InstanceBullet的GameObject,
 它由名为InstanceBullet的Prefab对象来描述,
 主要包含了一个表达子弹轨迹的长条形的mesh,和一个用于控制其飞行的脚本SimpleBullet.js。SimpleBullet.js
 function Update () {
 tr.position += tr.forward * speed * Time.deltaTime;function Update () {
 if (Time.time > spawnTime + lifeTime || dist < 0) {
 Spawner.Destroy (gameObject);SimpleBullet.js包含了一些参数,保证子弹有以下行为:
 沿创建的方向飞行
 有时限,时间到了会被休眠(Spawner.Destroy)
 有距离上限,超过距离会休眠(Spawner.Destroy)AutoFire.js
 bullet.dist = hitInfo.distance;
 除了上述2种方式消隐子弹实例外,子弹可以穿过场景里的石头,但是无法穿越箱子,也无法穿越将石头移开后露出的场景边界。
 这是因为在AutoFire做命中判定的同时,根据射线查询的结果调整了子弹的距离上限参数。3. 怪物的激活、攻击、动作控制原理,你所遇到的第一个怪物KamikazeBuzzer的攻击特效的实现原理
 [第1个KamikazeBuzzer]
 SimpleBuzzers7
 EnemyArea.js
 Box ColliderKamikazeBuzzer
 KamikazeMovementMotor.js
 BuzzerKamikazeControllerAndAi.js
 DestroyObject.js
 Health.js
 AudioSource[外形]
 buzzer_bot[动作]
 这个怪物的mesh没动作。[激活]
 EnemyArea.js
 function OnTriggerEnter (other : Collider) {
 if (other.tag == "Player")
 ActivateAffected (true);角色进入 Box Collider 的范围时,会触发OnTriggerEnter,
 这时会将SimpleBuzzers7的子对象KamikazeBuzzer设置为激活的。挂载到KamikazeBuzzer对象上的脚本组件也就可以开始执行了。[移动]
 KamikazeMovementMotor.js
 该脚本控制KamikazeBuzzer的刚体属性,根据参数和一定的计算规则计算出力,作用于刚体,让KamikazeBuzzer动起来,类似于Player的移动原理。BuzzerKamikazeControllerAndAi.js
 该脚本根据怪物和Player之间的位置关系,按一定计算规则算出KamikazeMovementMotor需要的参数,从而达到控制其移动的目的。direction = (player.position - character.position);
 因为方向总是朝着player,所以看起来就有个“追击”的效果。rechargeTimer < 0.0f && threatRange && Vector3.Dot (character.forward, direction) > 0.8f
 这个判断达到了“追过头”的效果。[攻击特效]
 当移动流程里“追到了”条件达成后,主要调用DoElectricArc函数来表达攻击方式。zapNoise = Vector3 (Random.Range (-1.0f, 1.0f), 0.0f, Random.Range(-1.0f, 1.0f)) * 0.5f;
 zapNoise = transform.rotation * zapNoise;
 这里有些小随机,是为了让每次电到Player的位置不一样。public var electricArc : LineRenderer;
 electricArc.SetPosition (0, electricArc.transform.position);
 electricArc.SetPosition (1, player.position + zapNoise);
 主要靠LineRenderer来描述闪电弧。
 LineRenderer用来构造若干条连续的线段,可以设置起始的宽度和结束的宽度。
 [被击]
 DamagePos(GameObject)
 Transform(Component)
 KamikazeBuzzer(GameObject)
 Health.js
 Health.js
 private var damageEffect : ParticleEmitter;
 function OnDamage (amount : float, fromDirection : Vector3) {
 damageEffect.Emit();DamagePos对象聚合了一个Transform组件,该组件为被击效果提供坐标信息。
 KamikazeBuzzer聚合了一个Health.js,其中的damageEffect指定为ElectricSparksHitA(prefab)。
 在之前子弹的命中流程中,被击中的target,会调用其Health组件的OnDamage函数。
 KamikazeBuzzer的Health的OnDamage,就是创建ElectricSparksHitA(Clone) 对象,从而达到播放被击特效。
 [死亡和爆炸]
 Health.js
 public var dieSignals : SignalSender;
 function OnDamage (amount : float, fromDirection : Vector3) {
 if (health <= 0)
 {
 dieSignals.SendSignals (this);SpawnObject.js
 function OnSignal () {
 spawned = Spawner.Spawn (objectToSpawn, transform.position, transform.rotation);DestroyObject.js
 function OnSignal () {
 Spawner.Destroy (objectToDestroy);当health值减少到0及0以下,对象就被判定为死亡了。
 DamagePos对象聚合了一个SpawnObject.js脚本。在其OnSignal函数里创建一个ExplosionSequenceBuzzer(prefab);
 ExplosionSequenceBuzzer是用来表达爆炸效果的。在其EffectSequencer.js脚本中,控制了一些粒子的变化。
 KamikazeBuzzer对象聚合了一个DestroyObject.js脚本。在其OnSignal函数里销毁了KamikazeBuzzer对象实例。4. 人物与怪相关的health处理相关流程
 Player和怪物的血量,都是通过聚合一个Health.js脚本组件来完成。
 伤害计算则是在各自的组件里独立编写计算的。Player是AutoFire,KamikazeBuzzer是在其AI脚本里。Health组件主要定义了
 血量
 被击特效
 受伤的痕迹
 被击事件
 死亡事件
 协作方式已经在分析Player和KamikazeBuzzer的行为方式时有所表述。5. 摄像机跟随与控制
 PlayerMoveController.js
 里面根据角色位置计算摄像机位置。根据鼠标位置,微调摄像机位置。
 6. 雨滴相关效果的实现原理,包括雨滴掉落、落到地面产生的波纹、地表水面的实现与反射效果等
 【雨滴】
 [相关GameObject]
 Rain 表达雨声
 RainBox 表达雨滴
 RainEffect 将各种东西组织起来的Root对象
 RainDrops 雨滴掉落的Root对象
 RainslpashesBig 表达雨滴的大涟漪的Root对象
 RainslpashesSmall 表达雨滴的小涟漪的Root对象
 splashbox 表达涟漪[Mesh&Material]
 RainDrops_LQ0/1/2
 RainsplashesBig_LQ0/1/2
 RainsplashesSmall_LQ0/1/2[Shader]
 Rain
 RainSplash[组织关系]
 Environment(dynamic)
 RainEffects(位置000)
 RainDrops(RainManager.js)
 RainBox*N
 RainBox.js
 Rain(Shader)
 RainDrops_LQ0(Mesh)
 RainslpashesBig(RainsplashManager.js)
 splashbox
 RainsplashBox.js
 RainSplash(Shader)
 RainsplashesBig_LQ0(Mesh)
 RainslpashesSmall
 splashbox
 RainsplashBox.js
 RainSplash(Shader)
 RainsplashesSmall_LQ0(Mesh)[落雨]
 RainManager.js
 function CreateMesh () : Mesh {
 public function GetPreGennedMesh () : Mesh {RainManager 在运行期创建了雨幕的Mesh和Material,思路为在固定大小的长方体里,随机生成只有4个顶点的小片。
 生成的对象和名字有关,即为 GameObject.name + _LQ0/1/2,一共3种类型的Mesh,只是生成的片的位置,uv坐标等不一致。RainBox.js
 function Update() {
 function OnDrawGizmos () {Update里的逻辑让雨幕Mesh在Y方向上从上自下的循环运动,从而达到雨滴落下的效果。
 OnDrawGizmos函数是为了在编辑期绘制雨幕的外形。[涟漪的创建]
 RainsplashManager.js
 RainsplashBox.js涟漪的创建方式和雨幕原理一样,只是在小片生成时的坐标,法线方向略有不同。
 大涟漪和小涟漪只是创建的片的数量和区域大小不同而已。[涟漪的扩散]
 RainSplash(Shader)里,对传进来的定点上的uv坐标和颜色做了一定的变换,从而做出涟漪从小变大和逐渐消隐。(疑问)材质从哪里指定的?inspector手工指定?
 (疑问)RainBox.js 里的enable,禁止和允许了哪些调用?【地表水面与反射】
 [相关GameObject]
 polySurface5097 地表
 RealtimeReflectionInWaterFlow.shader 处理水面模拟和反射的shader
 Main CameraReflectionMain Camera 反射摄像机
 RealtimeReflectionReplacement.shader 备用的shader方案
 Main Camera 主摄像机
 ReflectionFx.cs 生成反射贴图的脚本
 public System.String reflectionSampler = "_ReflectionTex"; 反射贴图
 reflectionMask[关键代码]
 ReflectionFx.cs
 public Transform[] reflectiveObjects;
 private Camera reflectionCamera;
 public LayerMask reflectionMask;
 计算反射摄像机的位置和朝向,将变换合并为反射矩阵
 渲染到反射贴图RealtimeReflectionInWaterFlow.shader
 _ReflectionTex("_ReflectionTex", 2D) = "black" {}
 v2f_full vert (appdata_full v)
 o.fakeRefl = EthansFakeReflection(v.vertex);
 fixed4 frag (v2f_full i) : COLOR0
 fixed4 rtRefl = tex2D (_ReflectionTex, (i.screen.xy / i.screen.w) + nrml.xy);
 rtRefl += tex2D (_FakeReflect, i.fakeRefl + nrml.xy * 2.0);RealtimeReflectionReplacement.shader
[LateUpdate]
 在LateUpdate里处理反射
 因为反射需要等所有对象的运动都结束了。[reflectiveObjects]
 reflectiveObjects是可以产生反射的对象,手工添加的,例子里有3个,分别是:
 polySurface5097
 polySurface425
 polySurface5095[helperCameras为什么要Clear]
 helperCameras从设计意图上来看,是为了支持游戏里任意数量的摄像机的反射,也就是ReflectionFx.cs的通用性。
 为了确保一帧之内只渲染一次反射贴图,所以就clear了。[被反射对象的筛选]
 reflectionMask
 可以被反射的对象必须是以下layer之一
 Reflection
 Player
 Enemies
 这个通过Inspector设置GameObject的Layer属性即可。7. 其它你觉着重要的主题
 Coroutine.协程,协程不是多线程,是将代码的执行控制权转移出去的一种机制。通过这种机制,可以让代码的执行流程不那么顺序化,达到各种模块协作的目的。
 AudioSource 音源对象,需要AudioClip载入声音资源,用AudioListener(ears)一起计算音频的声音大小。
 Animation 动画对象,控制骨骼动画相关,支持混合和IK。
 WWW 封装URL操作的类,可以支持http,https,file,ftp协议。其中ftp只能支持匿名登录。
 NGUI 一个轻量级UI库