研究一下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 Collider
KamikazeBuzzer
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庫
原文連結:http://blog.sina.com.cn/s/blog_4ef78af501013jvq.html