從零開(kāi)始獨(dú)立游戲開(kāi)發(fā)學(xué)習(xí)筆記32--Unity學(xué)習(xí)筆記15--Junior Programmer(上)

【超長(zhǎng)預(yù)警,萬(wàn)字警告】
Junior Programmer?是 Unity 的官方學(xué)習(xí)網(wǎng)站(不是中國(guó)特供的那個(gè))里,由官方提供的免費(fèi)教程之一。屬于初始教程系列的第二個(gè)。
其實(shí)之前也用過(guò)這個(gè)網(wǎng)站,當(dāng)時(shí)學(xué)的是 Unity Essential,是初始系列的第一個(gè)。
話(huà)不多說(shuō),直接開(kāi)始學(xué)。
4/29 隨筆:靠,本來(lái)以為一篇文章就能結(jié)束,結(jié)果發(fā)現(xiàn)比想象中多的多好多
4/30 隨筆:好像勉強(qiáng)也可以寫(xiě)進(jìn)一篇文章里
5/1 隨筆:不行,一篇文章還是不夠。Junior 這么猛的嗎。
1. Create with Code 1
比較簡(jiǎn)單,隨便看看即可。都是以前學(xué)過(guò)的很基礎(chǔ)的代碼。
2. Create with Code 2
2.1 聲音和效果
2.1.1 跳躍力
先導(dǎo)入素材,動(dòng)畫(huà)部分會(huì)有位置報(bào)錯(cuò),無(wú)視。刪掉 sample scene。進(jìn)入素材提供的 scene。
2.1.1.1 Sprite Renderer
進(jìn)入后可以看到一個(gè)背景圖片,點(diǎn)一下圖片,可以看到有一個(gè) sprite renderer 組件。
作用:
以前雖然也用過(guò),但是沒(méi)有仔細(xì)介紹過(guò),都是稀里糊涂的加上就完事了。
Sprite Renderer 主要是讓圖片能夠在場(chǎng)景上顯示出來(lái),能夠被看見(jiàn)。對(duì) 2D 游戲尤其重要。3D 偶爾用到圖片(諸如這次)的時(shí)候才會(huì)用到。
2.1.1.2 加上跳躍力
很簡(jiǎn)單,就簡(jiǎn)單說(shuō)一遍流程:
拖動(dòng)人物到 hierarchy。設(shè)置 rotation y 為 90,使得人物面向右方。
人物命名為 Player,加上 rigidbody 和 box collider。
在 Scripts 文件夾里創(chuàng)建腳本名為 PlayerController,拖到人物上。(如果直接在人物里添加組件,腳本文件會(huì)放到最外層文件夾,不利于文件位置管理)
創(chuàng)建一個(gè) RigidBody 類(lèi)型的私有變量,起名 playerRb。在 start 里賦值。
加上根據(jù)空格判斷是否跳躍。
中間遇到一個(gè) bug,unity editor 一直 hold on busy,原因是項(xiàng)目路徑上有中文。
2.1.1.3 AddForce
跳躍這里使用 AddForce。
第一個(gè) Vector 參數(shù)可以用 Vector3.up 語(yǔ)法糖,相當(dāng)于 Vector3(0,1,0)。
其實(shí) AddForce 還可以接受一個(gè) ForceMode 參數(shù),這里我們選用比較適合跳躍的 ForceMode.Impluse。

2.1.1.4 改進(jìn)跳躍力和重力
先把跳躍力變成變量暴露出來(lái),方便調(diào)整,這個(gè)沒(méi)什么好說(shuō)的以前講過(guò)。
然后重點(diǎn)是重力,重力調(diào)整也很簡(jiǎn)單,rigidbody 有一個(gè) gravity 屬性,調(diào)整即可。
public 一個(gè) float 變量,作為重力修改系數(shù)。
在 start 里,調(diào)用 Phsics.gravity 乘上這個(gè)修改系數(shù)來(lái)改變重力。這樣可以方便調(diào)整重力了。(注意 inspector 里的那個(gè)是質(zhì)量不是重力,別眼花了)。并且重力也不是屬于某個(gè) rigidBody 的屬性,而是一個(gè)作用于該 scene 上所有 rigidbody 的值。
選用系數(shù)而不是絕對(duì)值可以比較直觀的感覺(jué)到變化,畢竟你也不知道多少值算重多少算輕。

然后在游戲模式下調(diào)整到比較合適的參數(shù)即可。
2.1.1.5 防止無(wú)限跳
簡(jiǎn)單,略過(guò)。判斷一下是否在地面上即可。
2.1.1.6 障礙物
使用素材里的一個(gè) prefab 障礙物到場(chǎng)景中,修改一下。拖動(dòng)到自己的 Prefabs 文件夾里,變成一個(gè)新的自己的 prefab。
加上一個(gè)腳本起名為 MoveLeft。
在腳本里使用 transform.translate 來(lái)進(jìn)行移動(dòng),其中也可以用到 Vector3.left 語(yǔ)法糖。
把 MoveLeft 也賦給背景,造成人物在移動(dòng)的假象。
2.1.1.7 Spawn Manager
接下來(lái)我們用 Spawn Manager 來(lái)實(shí)例化 Prefab。和其他 Manager 一樣,新建一個(gè)空物體,掛載一個(gè)叫腳本。腳本代碼如下:

在游戲開(kāi)始的時(shí)候,在 SpawnPosition 上實(shí)例化一個(gè)障礙物。旋轉(zhuǎn)角度和 prefab 里的一致。
2.1.1.8 間隔安置 prefab
修改 SapwnManager 代碼如下:

把安置功能單獨(dú)拎出來(lái)。再在 Start 里生成一個(gè) InvokeRepeating。
參數(shù)作用顧名思義即可。
然后去 rigidbody 限制一下人物的旋轉(zhuǎn)和移動(dòng),免得人物亂跌倒。
2.1.2 讓世界更真實(shí)
2.1.2.1 無(wú)限背景圖
背景圖復(fù)制可以做到一樣的事,但是并不合理,很冗余。 只需要有一個(gè)前后對(duì)接并復(fù)制一次的背景圖。待到進(jìn)行到第一個(gè)和第二個(gè)背景圖交接處,重置背景圖位置到最開(kāi)始的位置即可。

先給背景圖加一個(gè) boxCollider,以便我們讀取背景圖大小。
代碼如下:

要點(diǎn):
生成變量的時(shí)候是 boxCollider 不是 collider,不然沒(méi)有 size 屬性。
_startPosition 是一個(gè) Vector3 而不是 float,因?yàn)闆](méi)法給 transform.position.x 單獨(dú)賦值。儲(chǔ)存一開(kāi)始的整個(gè) position 方便之后賦值。
_repeatWidth 是背景圖的一半長(zhǎng)度。
判斷重置背景的時(shí)候用的是 < 而不是 == ,因?yàn)橛?== 幾乎不可能那么巧剛好那一幀就一起重合了。(因此理論上這還不夠完美,如果是兩個(gè)背景 Object,在走到第二個(gè)背景的時(shí)候把第一個(gè)背景拼接到后面才是完美解)
2.1.2.2 游戲結(jié)束
很簡(jiǎn)單,都是以前學(xué)過(guò)的東西,當(dāng)撞到障礙物后,停止人物,障礙物,背景的移動(dòng)。并停止放置障礙物。
此外,障礙物調(diào)出視野外的時(shí)候,也需要銷(xiāo)毀。
2.1.3 動(dòng)畫(huà)
可以去人物自帶的 controller 里看一下。 雙擊動(dòng)畫(huà)可以看到實(shí)例動(dòng)畫(huà),或者點(diǎn)擊 transition 可以看到動(dòng)畫(huà)之間的轉(zhuǎn)變。
2.1.3.1 設(shè)置默認(rèn)動(dòng)畫(huà)
我們想讓人物一開(kāi)始就跑。因此直接在動(dòng)畫(huà)參數(shù)里設(shè)置 speed_f 為 1。因?yàn)?transition 里寫(xiě)的是超過(guò) 0.25 就會(huì)跑。
但是實(shí)際游玩會(huì)發(fā)現(xiàn),人物一開(kāi)始還是會(huì)有一瞬間的閑置動(dòng)畫(huà)。因此我們要把跑動(dòng)的動(dòng)畫(huà)設(shè)置為默認(rèn)狀態(tài)。

不過(guò)人物跑動(dòng)速度似乎還是慢了一點(diǎn),和背景比起來(lái)的話(huà)。其實(shí)我們可以去 inspector 里,改變動(dòng)畫(huà)的速度。

2.1.3.2 跳躍動(dòng)畫(huà)
看一下 animator 可以看到控制跳躍的是 Jump_trig,因此我們?cè)诖a里調(diào)用一下 SetTrigger 即可。
關(guān)于 trigger 的機(jī)制,之前有講過(guò),但不是特別清晰。這里有一個(gè)文章舉的例子很淺顯易懂。
2.1.3.3 死亡動(dòng)畫(huà)
簡(jiǎn)單。先看一下條件是哪些,然后代碼里觸發(fā)即可。
2.1.4 粒子和音效
在導(dǎo)入素材中有一些粒子,可以拖到 scene 里看一下,會(huì)發(fā)現(xiàn)場(chǎng)景右下角有一個(gè)工具框用來(lái)調(diào)試粒子效果。inspector 里也有很多巨多設(shè)置來(lái)控制粒子效果。 這里就把粒子速度調(diào)快一些 Velocity over Lifetime -> Speed Modifier。
然后把粒子拖到人物上,成為其子物體。
2.1.4.1 播放粒子動(dòng)畫(huà)
和 Animator 一樣,在代碼里引用 ParticleSystem 類(lèi)型后,調(diào)用 Play 方法。
這里先導(dǎo)入一個(gè)爆炸粒子,當(dāng)人物碰撞后播放。
2.1.4.2 人物奔跑粒子動(dòng)畫(huà)
添加一個(gè)人物跑動(dòng)的粒子。選中 Play On Awake 因?yàn)槲覀兇_實(shí)要一開(kāi)始就播放。 想改動(dòng)粒子的方向的話(huà),直接改粒子的 transform 就行。。。
在跳躍的時(shí)候停止動(dòng)畫(huà),落地的時(shí)候播放動(dòng)畫(huà)。游戲結(jié)束的時(shí)候停止動(dòng)畫(huà)。
有時(shí)候游戲結(jié)束人物依然有奔跑粒子。因?yàn)槿宋锟赡茉诳罩兴劳鋈缓舐涞兀涞睾蟛シ帕藙?dòng)畫(huà)。因此要加個(gè)判斷。
2.1.4.3 給攝像機(jī)添加音樂(lè)
簡(jiǎn)單,依舊是之前學(xué)過(guò)的內(nèi)容,添加一個(gè) Audio Source,拖動(dòng)一個(gè)音樂(lè)進(jìn) audio clip。調(diào)整 volume 小一點(diǎn)以防蓋住了之后要添加的音效。選上 loop 以循環(huán)播放。
2.1.4.4 音效
重點(diǎn),這里和之前 M_Studio 那里學(xué)的不一樣。
M_Studio 的做法是用一個(gè) SoundManager 來(lái)管理,里面只有一個(gè) AudioSource 組件,播放的時(shí)候會(huì)切換 AudioSource 的 AudioClip。
本教程則是把 AudioSource 當(dāng)做播放器來(lái)用,而不是 M_Studio 那樣把 AudioSource 當(dāng)做音樂(lè)本身來(lái)用。
具體表現(xiàn)在本教程我們不給 AudioSource 綁定任何 AudioClip。具體流程如下:
直接給 Player 添加一個(gè) AudioSource 組件,不加任何 AudioClip。
去到腳本里,創(chuàng)建兩個(gè)屬性,類(lèi)型為 AudioClip,用于跳躍和死亡,記得在 editor 里綁上文件。
新建一個(gè)屬性存儲(chǔ) AudioSource 組件,直接 GetComponent 獲取即可。
接下來(lái)使用 AudioSource 的一個(gè)新的方法,PlayOneShot。和之前的 Play 只能播放 AudioSource 上綁定的聲音不同,PlayOneShot 可以接受一個(gè)參數(shù)指定播放哪個(gè) AudioClip,也就是說(shuō)和自身綁定啥無(wú)關(guān),當(dāng)一個(gè)播放器來(lái)用,只要你能夠獲取到 AudioClip。
當(dāng)然我們第 3 步已經(jīng)獲取好了,這里直接放進(jìn)去即可。此外還可以接受第二個(gè)參數(shù),是一個(gè)浮點(diǎn)數(shù),表示播放音量。這里使用 1.0f 代表正常音量。(不加這個(gè)參數(shù)就是默認(rèn)正常音量)
注意:兩個(gè)教程的區(qū)別,重點(diǎn)在于 AudioSource 是作為和聲音深度綁定還是只是作為一個(gè)播放器來(lái)用。和新建到一個(gè) SoundManager 還是直接放在 Player 上沒(méi)有關(guān)系。本教程這個(gè)方式完全可以用 SoundManager 的形式弄。
2.1.5 小練習(xí)和測(cè)驗(yàn)
略過(guò)(但做了)
2.2 玩法機(jī)制
新建一個(gè)新的項(xiàng)目,導(dǎo)入素材。
2.2.1 熟悉場(chǎng)景
可以看到這次的攝像頭有點(diǎn)不一樣,因?yàn)檫@次的攝像頭采用的不是透視投影而是正交投影。因此我們?cè)?scene 里也調(diào)成幾何視角保持和游戲界面一致。

新建一個(gè)球體,放到場(chǎng)景中央。
放大一下 scale。
添加一個(gè) texture,直接拖到場(chǎng)景中的球上即可。
2.2.1.1 添加聚焦點(diǎn)
我們希望我們的攝像頭圍繞著中央旋轉(zhuǎn)來(lái)看這個(gè)平臺(tái)。
為了實(shí)現(xiàn)這個(gè)目的,我們需要對(duì)攝像頭進(jìn)行操控,為了方便操作,我們新建一個(gè)空物體命名為 Focal Point,然后把攝像機(jī)成為其子物體。(如果你學(xué)過(guò) AE,這是一個(gè)很常見(jiàn)的手段)
這個(gè)時(shí)候你可以改變一下 Focal Point 的 y 軸旋轉(zhuǎn)度,這正是我們要的效果。
2.2.1.2 旋轉(zhuǎn)
綁定一個(gè)腳本,里面通過(guò)鍵盤(pán)的左右控制旋轉(zhuǎn)。

2.2.1.3 移動(dòng)
給 Player 添加腳本,用上下控制移動(dòng)。

2.2.1.4 移動(dòng)跟隨旋轉(zhuǎn)角度
現(xiàn)在移動(dòng)只會(huì)朝著某一個(gè)方向移動(dòng),因?yàn)?Vector3.forward 指向的永遠(yuǎn)是同一個(gè)方向,而我們需要移動(dòng)隨著鏡頭旋轉(zhuǎn)而改變。
之前有提到過(guò) global 和 local,會(huì)影響工具的角度。但當(dāng)時(shí)以為 transform 會(huì)跟著改變。其實(shí)不會(huì),transform 反映的是 global 的位置,角度和縮放。當(dāng)你把物體換成 local 后再斜方向移動(dòng),會(huì)看到 transform 會(huì)同時(shí)改變兩個(gè)軸的值。
注意:雖然是 global,但是并不是 3D 空間的絕對(duì)位置,而是相對(duì)于 parent 物體的 transform 的相對(duì)位置。只有最外層的物體才是真實(shí)絕對(duì)位置
此外,面板里和代碼里是不一樣的,代碼里的 transform.position 是絕對(duì)位置,想要取得相對(duì)位置有專(zhuān)門(mén)的 localPosition
換句話(huà)說(shuō),global 和 local 只影響工具的角度,其他不變。
代碼里通過(guò)如下方式獲取信息:

首先獲取 focal point 的 transform。
通過(guò) transform.forward 獲取藍(lán)軸信息。
2.2.2 跟隨 Player
2.2.2.1 物理材質(zhì)
再來(lái)看一遍這個(gè) inspector:

屬性很少:
動(dòng)態(tài)和靜態(tài)摩擦力。表示物體靜止和移動(dòng)時(shí)候的摩擦力。
彈力,碰撞的時(shí)候彈回的力。
Friction combine 和 Bounce Combine 是用于當(dāng)兩個(gè)同樣具有物理材質(zhì)的物體相撞的時(shí)候采用的計(jì)算方式。average 就是加起來(lái)除以 2,minumum 和 maximum 是取兩者最小或最大,multiply 就是相乘。
可以發(fā)現(xiàn)這些屬性都和碰撞有關(guān),只有碰撞的時(shí)候需要用到。因此物理材質(zhì)是綁在 collider 上的。
這里我們新建一個(gè)球體敵人,給其和 Player 都套上一個(gè)新建的物理材質(zhì)。改變一下彈力以及 combine 方式??纯葱Ч?。
2.2.2.2 敵人跟隨玩家
直接看代碼:

這是掛載到敵人身上的代碼:
獲取自身 rb 和玩家的 object。
這里看一下 AddForce 的參數(shù),主要是前面這句?
(this.Player.transform.position - transform.position).normalized
,括號(hào)內(nèi)的部分是玩家的位置減去敵人的位置,這是什么意思呢?其實(shí)仔細(xì)想想,位置是一個(gè) Vector3 向量,玩家的位置向量減去敵人的位置向量,實(shí)際上就是一個(gè)從敵人指向玩家的向量。不過(guò)如果直接這么使用的話(huà),玩家離敵人越遠(yuǎn),敵人的攻擊欲望就越強(qiáng),因?yàn)榫嚯x變大了,方向向量也變大了。如果不想要這種效果的話(huà),那就需要把方向向量 normalize,讓其只用作為方向的功能。這里 Vector3.normalized 返回一個(gè)長(zhǎng)度為 1 的同方向的向量。
2.2.2.3 spawn manager
依然是用 spawn manager 來(lái)生成敵人,過(guò)程不再贅述。

可以把生成隨機(jī)位置的代碼拿出來(lái)做一個(gè)方法,這一操作在 VS 里有簡(jiǎn)便操作(選中想要提取的代碼行,按 alt+enter 或者左邊的燈泡圖標(biāo),選擇提取方法即可):

2.2.3 Powerup 和 Countdown
2.2.3.1 Powerup
生成一個(gè) powerup 道具,撿起的時(shí)候 player 變厲害。
trigger 檢測(cè) powerup 道具,trigger 后銷(xiāo)毀道具,player 改變狀態(tài)。
碰撞檢測(cè)敵人,在有 powerup 的情況下,與敵人碰撞后將其擊飛。
擊飛代碼邏輯其實(shí)用之前講過(guò)的內(nèi)容即可做到,可以試一下。
答案其實(shí)就是用敵人的位置減去自己的位置,得到方向,然后給敵人一個(gè) Impluse 的 AddForce。
2.2.3.2 Countdown
我們想要做一個(gè)倒計(jì)時(shí),獨(dú)立于 update 之外能夠自己定時(shí)更新。先看代碼:

這里用到了接口,接口一般命名以 I 開(kāi)頭。這里用到的接口經(jīng)常用于類(lèi)似于倒計(jì)時(shí)這樣的場(chǎng)景。
該接口用到了 yield 關(guān)鍵字。會(huì)讓代碼等待在這一行,直到該行運(yùn)行完畢再進(jìn)行下一行。
剛剛提到了運(yùn)行完畢,因此 yield 后面返回的得是一個(gè)異步過(guò)程。而這里我們使用了 WaitForSeconds,這是一個(gè)異步過(guò)程,作用顧名思義,等待 x 秒。
等待這 x 秒過(guò)去后,yield 返回值,并繼續(xù)下一行,把 _isPowerup 返回 false。
上面可以看到在 trigger 里使用?
StartCoroutine(PowerupCountdownRoutine())
?,注意括號(hào)里是接口的調(diào)用,而不是接口本身。畢竟我們 StartCoroutine 是要接受一個(gè)協(xié)程作為參數(shù)的,而不是 IEumerator。StartCoroutine 會(huì)起一個(gè)協(xié)程,而這個(gè)協(xié)程是獨(dú)立于 update 之外的。這樣就達(dá)到了我們的目的。
實(shí)際上剛剛所做的,用 Invoke 很簡(jiǎn)單就達(dá)到了。Invoke 就是一個(gè)可以簡(jiǎn)單使用的 coroutine。
2.2.3.3 Powerup Indicator
一個(gè) powerup 指示器。用 setactive 即可實(shí)現(xiàn)。
不過(guò)也有坑,就是如果只是單純把 indicator 作為 player 的子物體。會(huì)發(fā)現(xiàn) indicator 會(huì)跟著一起旋轉(zhuǎn)。這不是我們想要的。
解決方法有很多種,教程里干脆直接不用子物體,而是在 update 里實(shí)時(shí)更新 indicator 的位置為 player 的位置。
2.3 UI
說(shuō)是 UI,其實(shí)還是一個(gè)小游戲項(xiàng)目。大概就是做一個(gè)點(diǎn)擊的小游戲,有些物體點(diǎn)了加分,有些點(diǎn)了扣分這種。
2.3.1 鼠標(biāo)點(diǎn)擊
Unity 提供了很多鼠標(biāo)點(diǎn)擊相關(guān)功能?,F(xiàn)在我們就來(lái)嘗試一下。
2.3.1.1 批量添加組件
一開(kāi)始需要給之后用到的好物體和壞物體加上 rigidbody,collider 和腳本。一個(gè)一個(gè)加顯然低效。其實(shí) inspector 是可以顯示批量物體的詳情的。選中多個(gè)物體,會(huì)顯示這些物體中重復(fù)的組件。也可以批量添加新組件。
2.3.1.2 隨機(jī)上拋
我們要隨機(jī)向上拋出物體。除了之前用到的 AddForce,我們還會(huì)用到 AddTorque,用于讓物體物理式旋轉(zhuǎn)。

2.3.1.2.1 測(cè)試小 tip
因?yàn)槲覀冞€沒(méi)寫(xiě) spwanManager,但是想測(cè)試一下上拋的效果怎么樣。一種是先拖一個(gè)物體到場(chǎng)景中,但這樣有些笨重,測(cè)試多個(gè)效果的時(shí)候,測(cè)完了還要一個(gè)一個(gè)刪除。
還有種方式是運(yùn)行游戲后,再往場(chǎng)景里拖物體,拖進(jìn)去后代碼立刻生效。(換句話(huà)說(shuō)你就是 SpawnManager)而且因?yàn)槭怯螒蚰J?,退出后這些物體也會(huì)一并消失,很方便。
2.3.1.3 Game Manager
這次邏輯比較多,因此 spwan manager 被整合進(jìn) game manager 里,這里就直接建一個(gè) game manager 來(lái)代替 spwan manager 以及后面會(huì)用于管理各種狀態(tài)了。

這里用 IEnumerator 其實(shí)也就是實(shí)現(xiàn)了一個(gè) InvokeRepeating 而已。
使用了列表,列表的長(zhǎng)度是 Count 不是 Length 噢。
2.3.1.4 鼠標(biāo)點(diǎn)擊
Unity 對(duì)于鼠標(biāo)點(diǎn)擊事件有專(zhuān)門(mén)的函數(shù),會(huì)在鼠標(biāo)點(diǎn)擊到這個(gè)物體的時(shí)候觸發(fā),所以我們代碼如下即可:

這里是 OnMouseDown,當(dāng)然同理還有 OnMouseUp,OnMouseOver,OnMouseExit 等等。
2.3.2 顯示分?jǐn)?shù)
這里開(kāi)始終于講 UI 了,不過(guò)用的依舊是老版的 UI 系統(tǒng),因此不會(huì)進(jìn)行過(guò)多闡釋。但是做還是要跟著做的。無(wú)論如何,學(xué)習(xí)一個(gè)系統(tǒng)的歷史也能夠有更開(kāi)闊的視角去看待新系統(tǒng)的一些功能。
2.3.2.1 銷(xiāo)毀粒子效果
當(dāng)鼠標(biāo)點(diǎn)擊的時(shí)候,在物體位置上 Instantiate 一個(gè)粒子。勾選 Play on Awake。并給粒子的 stop action 換成 Destroy,這樣動(dòng)畫(huà)播完粒子就會(huì)自動(dòng)銷(xiāo)毀了。(或者也可以在代碼里使用 Destroy 第二個(gè)參數(shù),可以延遲銷(xiāo)毀)
2.3.3 Game Over
不細(xì)講,照做一遍即可。順帶一提 TextMeshPro -button 其實(shí)是一個(gè)舊的 button 里面套著一個(gè) TextMeshPro -text。因此引用的時(shí)候這么寫(xiě):

其實(shí)所謂的 TextMeshProUGUI 就是這個(gè)組件↓:

能不能引用其實(shí)就看有沒(méi)有帶這個(gè)組件。
2.3.4 難度選擇
依舊不細(xì)講。不過(guò)有一些額外知識(shí):
2.3.4.1 UnityEvents相關(guān):addListener 其實(shí)就是相當(dāng)于在 Inspector 里點(diǎn)加號(hào)。
以下兩個(gè)是相等的。


這也解釋了為什么在自定義事件 UnityEvents 的文檔里寫(xiě)著,Invoke 會(huì)執(zhí)行所有的 AddListener,這樣就很容易理解了。畢竟你全都綁在 OnClick 上了,自然當(dāng) Click 的時(shí)候就會(huì)執(zhí)行所有的綁定事件。
而 Invoke 的作用自然也清晰了,當(dāng)你自定義事件的時(shí)候,不是你起個(gè)名字叫做 MouseClick 就能識(shí)別鼠標(biāo)點(diǎn)擊了,自然需要你寫(xiě)一個(gè)抓取鼠標(biāo)點(diǎn)擊的函數(shù),然后在里面調(diào)用 Invoke,以此來(lái)實(shí)現(xiàn)事件。
2.3.5 練習(xí)
沒(méi)什么好講的,一些小 tips
2.3.5.1 更換素材
有時(shí)候我們想用新素材替換掉舊素材,但是原素材上已經(jīng)有很多組件了并不好操作。
有一個(gè)方法是把新素材作為原素材的子物體,然后取消原物體的 mesh renderer。如果想改變 collider 也可以取消原物體的 collider,以此類(lèi)推。
2.4 用戶(hù)反饋和測(cè)試
居然開(kāi)始講用戶(hù)測(cè)試了,震驚,不過(guò)確實(shí)很重要,仔細(xì)學(xué)一下。
但是也不用特別在意,因?yàn)殛P(guān)于游戲測(cè)試的事項(xiàng),《游戲設(shè)計(jì)藝術(shù)》里有更詳細(xì)的講述。我會(huì)在那個(gè)路線(xiàn)里重點(diǎn)講(不過(guò)是書(shū)里很后面的內(nèi)容了,應(yīng)該需要一段時(shí)間才能走到)。這里看看就行。
2.4.1 是什么
這個(gè)就不用說(shuō)了,大家都知道。不過(guò)需要注意的是,用戶(hù)測(cè)試不是個(gè)一次性流程,而是應(yīng)該貫穿整個(gè)制作流程,得到反饋再來(lái)改進(jìn)游戲的一個(gè)循環(huán)過(guò)程。
2.4.2 為什么要有一個(gè)結(jié)構(gòu)化的測(cè)試流程
好的用戶(hù)測(cè)試并不是給找一群人,給他們玩游戲,然后得到不同的信息反饋。當(dāng)然這也有用。但是好的用戶(hù)測(cè)試應(yīng)該是一個(gè)結(jié)構(gòu)化系統(tǒng),盡可能全方位地從測(cè)試?yán)锩娅@取各式各樣的信息。包括區(qū)分測(cè)試人群進(jìn)行不同人群的比對(duì)等等。
理想狀態(tài)下,你應(yīng)該在游戲開(kāi)發(fā)的不同階段進(jìn)行測(cè)試。
如果在游戲開(kāi)發(fā)完成度比較高的時(shí)候去測(cè)試,很多東西可能已經(jīng)不好更改了。這還不只是說(shuō)玩法設(shè)計(jì)上面的不好更改。有些 bug 如果是牽一發(fā)動(dòng)全身的,這時(shí)候也就已經(jīng)很難辦了。
2.4.3 用戶(hù)測(cè)試的階段
用戶(hù)測(cè)試大概分這幾個(gè)階段。
設(shè)定目標(biāo)。
計(jì)劃 session。(我不知道怎么翻譯比較好)
執(zhí)行跟進(jìn) session。
評(píng)估結(jié)果。
2.4.3.1 計(jì)劃用戶(hù)測(cè)試(包含 1, 2 兩步)
2.4.3.1.1 設(shè)定目標(biāo)
問(wèn)自己兩個(gè)問(wèn)題:
你想從這個(gè)測(cè)試中獲取什么信息?
你要給用戶(hù)什么來(lái)測(cè)試?(一個(gè)小關(guān)卡?一個(gè)原型?一個(gè)框架?還是一個(gè)完整的游戲?)
2.4.3.1.2 準(zhǔn)備問(wèn)題
測(cè)試的時(shí)候要問(wèn)用戶(hù)問(wèn)題的,我門(mén)要準(zhǔn)備一些開(kāi)放性問(wèn)題,一些固定回答問(wèn)題,或者兩者結(jié)合:
開(kāi)放性問(wèn)題:用戶(hù)能夠自由回答,類(lèi)似于填空題。比如說(shuō)你玩的時(shí)候哪里感到最開(kāi)心?你覺(jué)得游戲好不好玩?之類(lèi)的。
固定回答問(wèn)題:只給用戶(hù)一些選擇去選,類(lèi)似于選擇題。比如說(shuō)讓用戶(hù) 1-5 打個(gè)分,或者只能選是或者否的一些問(wèn)題。
固定回答更適合問(wèn)卷。而開(kāi)放性問(wèn)題則最好是當(dāng)面問(wèn),因?yàn)橛脩?hù)沒(méi)法和你直接交流,或者用戶(hù)趕時(shí)間就填了一些不太好理解的話(huà)。你也沒(méi)法繼續(xù)問(wèn)深入討論下去。
2.4.3.1.3 計(jì)劃 session
session 就是測(cè)試用戶(hù)已經(jīng)拿到游戲了,開(kāi)始玩游戲了,在給你反饋之前的這一時(shí)間段。
這個(gè)時(shí)間段也是需要好好計(jì)劃的,不是上來(lái)就直接讓他們玩,有這些問(wèn)題需要考量:
確認(rèn)好日期和時(shí)間。比如說(shuō)你的測(cè)試用戶(hù)是上班族,那你最好別開(kāi)在工作日白天。
根據(jù)你游戲的目標(biāo)用戶(hù),找對(duì)應(yīng)的測(cè)試人員。比如說(shuō)你想做給 rpg 老手玩,那你就找一些 rpg 老鳥(niǎo)。你想給沒(méi)接觸過(guò) rpg 的人玩,那你應(yīng)該找那些從來(lái)沒(méi)玩過(guò) rpg 的人來(lái)測(cè)。
確定好測(cè)試人數(shù)。(這個(gè)我不確定,不是越多越好嗎)
最好能把整個(gè)測(cè)試過(guò)程記錄下來(lái)。(如果可能的話(huà),比如錄屏,記錄盡可能多的游戲生成日志,等等)
如果你是一個(gè)團(tuán)隊(duì)的話(huà),可以找一個(gè) leader 和一個(gè)記錄人。
2.4.3.2 session 開(kāi)始前的準(zhǔn)備(第 2,3 步)
以下 tips 對(duì)你有幫助。
2.4.3.2.1 觀察,而不是指導(dǎo)
用戶(hù)卡住的時(shí)候不要去告訴他怎么做。這是一個(gè)信息告訴你你的引導(dǎo)不太行。
如果卡很久的話(huà),你可以選擇告訴他。以便讓他繼續(xù)接下來(lái)的測(cè)試。
2.4.3.2.2 不要解釋你的設(shè)計(jì)思路
用戶(hù)遇到了一些問(wèn)題,也許確實(shí)是走錯(cuò)了路,然后你去跟他解釋?zhuān)苡锌赡芩托欧恕?br>但是這并不是一個(gè)好的測(cè)試,因?yàn)槠渌撕苡锌赡苡龅较嗤膯?wèn)題,你不可能一一向所有人解釋。
2.4.3.2.3 不要在測(cè)試的時(shí)候解決問(wèn)題
不要在用戶(hù)告訴你某個(gè)問(wèn)題之后,你就立刻去關(guān)注這個(gè)問(wèn)題,想出解決方案。測(cè)試的目的在于搜集問(wèn)題。如果用戶(hù)發(fā)現(xiàn)了一個(gè)問(wèn)題,你就記錄下來(lái),然后讓用戶(hù)趕緊去看下一個(gè)問(wèn)題。而不是和你討論這個(gè)問(wèn)題怎么解決。
包括測(cè)試用戶(hù)自身也是,很多用戶(hù)測(cè)試的時(shí)候甚至?xí)湍阆脒@個(gè)問(wèn)題可以怎么解決,你應(yīng)該引導(dǎo)他們?nèi)フ蚁乱粋€(gè)問(wèn)題,而不是在在這個(gè)問(wèn)題上花費(fèi)過(guò)多時(shí)間。
解決問(wèn)題應(yīng)該在測(cè)試完畢之后。
2.4.3.3 執(zhí)行跟進(jìn) session(第 3 步)
每個(gè) session 都不太一樣,但基本結(jié)構(gòu)一般都是如下:
向你的用戶(hù)介紹這個(gè)測(cè)試的一些情況。
確認(rèn)好 session 的目標(biāo)。
向用戶(hù)介紹記錄方式,確認(rèn)能否接受。
提供測(cè)試資源,觀察用戶(hù)測(cè)試。
問(wèn)之前準(zhǔn)備好的問(wèn)題。
總結(jié)用戶(hù)的答案。
結(jié)束
2.4.3.3.1 一些跟進(jìn)的 tips
我決定不寫(xiě)出來(lái)。因?yàn)楹芏嗪陀螒蛟O(shè)計(jì)藝術(shù)里說(shuō)的沖突,而我更偏向于那本書(shū)里的內(nèi)容。感興趣可以自己去看。
2.4.3.4 結(jié)束 session 之后
做完 session,立刻做以下的事情:
看看記錄有沒(méi)有記錄好。
把所有想的東西都寫(xiě)下來(lái)。
和測(cè)試者討論一些細(xì)節(jié)問(wèn)題。
寫(xiě)個(gè)總結(jié)文檔。
決定哪些反饋需要更改,哪些不用。
2.5 如何進(jìn)階
按照課程的說(shuō)法,學(xué)到這我已經(jīng)完全入門(mén)了。這一節(jié)會(huì)告訴我一些各種各樣的 tips,讓我比較安穩(wěn)地走上這條路。
2.5.1 項(xiàng)目?jī)?yōu)化
之前的項(xiàng)目,并不是特別完美。有很多特性可以幫我們更好地完成任務(wù)。有些代碼寫(xiě)的不盡如意。現(xiàn)在來(lái)一一優(yōu)化。(不過(guò)例子里用的是很老的 unity editor 版本了,感覺(jué)還是會(huì)有很多特性沒(méi)講到)
2.5.1.1 變量屬性
之前用到的變量很少,也就 public,private。(雖然我本人還用了 static, [SeralizedField]) 這里介紹了一些別的,類(lèi)似于 static, const, readonly, [SeralizedField]。沒(méi)什么好講的。
2.5.1.2 別的事件函數(shù)
之前只用到了 start 和 update。實(shí)際上還有 FixedUpdate, 和 LateUpdate 等等非常多。
FixedUpdtae 以前講過(guò),總之就是特別適合進(jìn)行物理計(jì)算。順便一提,F(xiàn)ixedUpdate 會(huì)在 Update 之前執(zhí)行,這個(gè)之前沒(méi)有提到過(guò)。
LateUpdate 則是特別適合進(jìn)行攝像機(jī)位置計(jì)算。當(dāng)你需要控制攝像機(jī)位置的時(shí)候,一般用這個(gè)比較好。尤其是當(dāng)你發(fā)現(xiàn),有時(shí)候攝像機(jī)跟隨物體的時(shí)候,總是有點(diǎn)微小的晃動(dòng)的時(shí)候。原理也很簡(jiǎn)單,因?yàn)樵谖锢碛?jì)算之后,攝像機(jī)就能夠準(zhǔn)確地算出下一步應(yīng)該去哪里。
Awake。Awake 在 Start 之前。Awake 會(huì)在腳本實(shí)例化的時(shí)候調(diào)用。而 Start 會(huì)在這個(gè)實(shí)例被啟用的時(shí)候調(diào)用。也就是說(shuō)即使禁用了這個(gè)腳本組件,awake 也會(huì)調(diào)用,不過(guò) Start 就不會(huì)調(diào)用了。(不過(guò)如果整個(gè)對(duì)象都被禁用了(如 setActive(false)),那 awake 也不會(huì)被調(diào)用)
2.5.1.3 對(duì)象池
之前新建 object 的方式比較低效。對(duì)象池能夠更高效地進(jìn)行操作。
2.5.1.3.1 導(dǎo)出項(xiàng)目
如果想備份一下目前的項(xiàng)目,可以在 Assets -> Export Package 里操作(2022 版 unity editor)。
2.5.1.3.2 對(duì)象池的思想
像是之前生成物體都是通過(guò)預(yù)制體來(lái) Instantiate,但是這個(gè)過(guò)程其實(shí)還挺耗費(fèi)性能的。好不容易實(shí)例化了一個(gè),之后又要?jiǎng)h掉再重新實(shí)例化。
而對(duì)象池的思想就是在一開(kāi)始就生成一大堆對(duì)象組成的對(duì)象池,然后需要用到的時(shí)候激活或者停用。(沒(méi)錯(cuò),就是那個(gè) SetActive)。所以其實(shí)很簡(jiǎn)單。在一開(kāi)始的時(shí)候根據(jù)需要生成多個(gè) SetActive(false) 的物體。需要使用的時(shí)候就激活,然后把位置調(diào)到對(duì)應(yīng)的地方。
可以看一下其中一種實(shí)現(xiàn)代碼:

2.5.3 搜索 和 解決 bugs
好家伙,看了一眼,居然在認(rèn)真教你怎么搜索比較好。
不在這個(gè)推薦算法的時(shí)代,很多人確實(shí)不太會(huì)用搜索引擎。如果你平時(shí)很少用搜索引擎,確實(shí)可以看一下。
不只是教你怎么用,還教你有些時(shí)候需要搜索兩遍的情況,比如說(shuō)第一次搜做某個(gè)功能應(yīng)該用到什么函數(shù),第二次搜索怎么用這個(gè)函數(shù)。等等。
很多時(shí)候都可以直接搜出一個(gè)解決方案出來(lái)。相比之下,一些用的人少的游戲引擎,就還挺難搜的。
2.5.4 分享項(xiàng)目
就是打包,或者打包成 webgl 發(fā)布到網(wǎng)站。都是學(xué)過(guò)的內(nèi)容了。
2.5.5 ECS 生存指南
好家伙,什么東西啊這是??
懵逼中,不過(guò)看了一下,全稱(chēng)是 Entity Component System。是屬于新的 DOTS(數(shù)據(jù)導(dǎo)向技術(shù)棧)的一部分。表示一種新的編程思想。
和之前的 Unity 編程系統(tǒng)不一樣(基于 Object 和 Component,像是 EnemyPrefab,Collider 這之列的),新系統(tǒng)會(huì)基于 data(基于像是移動(dòng)速度,位置,生命值這之類(lèi)的)。
用這種系統(tǒng)的原因,就是因?yàn)檫@樣可以極大的提高運(yùn)行效率。
2.5.5.1 簡(jiǎn)單介紹
ECS 是比較深入的知識(shí)。因此這里就只是簡(jiǎn)單介紹一下。
先從 DOTS 說(shuō)起,DOTS 全稱(chēng)是 Data-Oriented Technology Stack,一個(gè)技術(shù)棧,自然包含了多個(gè)技術(shù):
ECS
C# Job System
The Burst Compiler
數(shù)據(jù)導(dǎo)向,意味著代碼主要是圍繞著數(shù)據(jù),而不是 Class 來(lái)設(shè)計(jì)。
能夠和很好地提升性能。教程給了個(gè)視頻,畫(huà)面很好,并且 60 幀。最重要的是,在手機(jī)上跑的。
然后大概給看了一下新代碼長(zhǎng)啥樣:

2.6 求職
不需要,且不符合國(guó)情,跳過(guò)。