基礎5——FPS——對性能的測量
匯總
雪之下雪乃:純文字和圖片的Unity教程-系列概述和匯總純文字和圖片的Unity教程-系列概述和匯總雪之下雪乃:純文字和圖片的Unity教程-系列概述和匯總本節將會涉及
- 利用Unity的物理特性創建不斷增大的原子核
- 利用Unity的profiler來分析性能
- 測量和展示幀率
- 避免創建臨時字元串
- 穩定幀率
- 給幀率的展示上顏色
我們在這節中將會創建一個簡單的測試場景並測量幀率。我們先用profiler,之後創建一個我們自己的幀率計數器
同樣,在看這節之前請先確保看過前面的教程。
1創建原子核
我們要對性能進行測試,因此創建一個越來越大的原子核,這樣性能會有一個從好到壞的變化
當然這裡的原子核只是一個簡單的模擬,我們不追求重現現實世界中的原子核
我們創建一個原子核(Nucleus)由核子組成(Nucleon)我們以核子為基本的單位。首先創建一個核子腳本
RequireComponent特性會為遊戲物體自動添加相應的組件
using UnityEngine;[RequireComponent(typeof(Rigidbody))]public class Nucleon : MonoBehaviour { public float attractionForce; Rigidbody body; void Awake () { body = GetComponent<Rigidbody>(); } void FixedUpdate () { body.AddForce(transform.localPosition * -attractionForce); }}
核子分為質子(proton)和中子(neutron)兩種,我們用球做兩個預製體,並分別給它們創建紅色和藍色的材質
暫時把引力設為10
然後創建一個生成器腳本。我們需要知道生成的間隔時間,生成的舉例和生成對象
using UnityEngine;public class NucleonSpawner : MonoBehaviour { public float timeBetweenSpawns; public float spawnDistance; public Nucleon[] nucleonPrefabs;}
創建一個空物體掛載這個腳本,並給欄位賦值
添加生成的方法,在質子和中子之間隨機選擇,並在FixedUpdate中調用。另外添加一個欄位來記錄距上次生成的時間
float timeSinceLastSpawn; void FixedUpdate () { timeSinceLastSpawn += Time.deltaTime; if (timeSinceLastSpawn >= timeBetweenSpawns) { timeSinceLastSpawn -= timeBetweenSpawns; SpawnNucleon(); } } void SpawnNucleon () { Nucleon prefab = nucleonPrefabs[Random.Range(0, nucleonPrefabs.Length)]; Nucleon spawn = Instantiate<Nucleon>(prefab); spawn.transform.localPosition = Random.onUnitSphere * spawnDistance; }
Random.onUnitSphere會取得在離原點半徑為1的球表面的一個隨機點
這時運行,就可以看到一個不斷聚集的原子核啦。當原子核達到一定程度時,就會發現幀率明顯變慢了
可以通過減小我們自定義的timeBetweenSpawns,來增加生成的速度。
同時在 Edit / Project Settings / Time中有幾個Unity自帶的屬性
Fixed Timestep:減小這個值會讓每秒的物理計算增多
Time Scale:這個就是遊戲中時間的速度,默認是和現實世界中一樣的一比一。
2使用Profiler
這時可以打開Game中的Stats來看FPS
但是這裡的幀率並不是準確的,只是一個大概的計算。
我們打開Window / Profiler觀察這裡的數據,這才是這節的主題
我們把垂直同步vsync,關掉
在Edit / Project Settings / Quality中
這時運行,可以看到一開始FPS簡直高得飛起來,這會對硬體有很大的消耗。
我們可以看到兩個顯著的特點:
一是CPU usage會周期性的突起
二是在內存圖中的GC allocation也會偶爾突起
這兩個都是由Unity Editor本身造成的。如果我們把項目打包成可執行文件,就會看到不同的數據
這是真機測試的數據,剛才兩個特點都不見了
3測試FPS
接下來我們要創建自己的PFS計數器
首先創建腳本FPSCounter,添加屬性FPS,並在Update中計算它的值
時間用unscaledDeltaTime為了不受Time Scale的影響
using UnityEngine;public class FPSCounter : MonoBehaviour { public int FPS { get; private set; } void Update() { FPS = (int)(1f / Time.unscaledDeltaTime); }}
接下來就是用UI來顯示我們的FPS了
創建一個UI物體會自動生成一個EventSystem來處理輸入,但是我們不需要輸入處理,所以可以刪掉。
層級結構如下,其中FPS Panel就是一個panel, FPS Label是text
Canvas選上Pixel Prefect, 雖然會更耗費性能
這樣一個UI素材本身的像素對應屏幕上一個像素的情況,UI素材映射到屏幕上時沒有任何拉伸和壓縮,顯示效果非常清晰完美。
其他採用默認的參數
把我們的UI放到屏幕的一角
然後創建腳本FPSDisplay,掛在FPSPanel上。
我們使用RequireComponent特性,所以FPSCounter也會連帶掛上去。
然後給FPSLabel欄位賦值
在Display中創建一個counter對象,每幀給text更新數值
FPSCounter fpsCounter; void Awake () { fpsCounter = GetComponent<FPSCounter>(); } void Update () { fpsLabel.text = fpsCounter.FPS.ToString(); }
這時運行起來可以看到幀率的變化了,但是由於我們設計只顯示兩位數,所以我們限制一下顯示的數值
Clamp方法第一個參數是要限制的對象,第二和三個參數是要限制的範圍。
void Update () { fpsLabel.text = Mathf.Clamp(fpsCounter.FPS, 0, 99).ToString(); }
但是這時每次Update都會創建一個臨時string給text顯示,這會消耗性能,也使我們的觀測數據不準確
我們可以使用一個固定數組來解決這個問題
static string[] stringsFrom00To99 = { "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99" }; void Update () { fpsLabel.text = stringsFrom00To99[Mathf.Clamp(fpsCounter.FPS, 0, 99)]; }
4平均FPS
現在我們是每幀更新UI上的FPS,這會非常難讀。
我們在Counter中新加一個欄位
public int frameRange = 60;
然後我們把屬性的名字從FPS 改成AverageFPS
public int AverageFPS { get; private set; }
再創建一個數組來緩存每幀的數據
int[] fpsBuffer; int fpsBufferIndex;
創建一個方法來初始化buffer,frameRange不能為負
void InitializeBuffer () { if (frameRange <= 0) { frameRange = 1; } fpsBuffer = new int[frameRange]; fpsBufferIndex = 0; }
我們把UpdateBuffer和CalculateFPS都封裝一下,在Update裡面調用
void Update () { if (fpsBuffer == null || fpsBuffer.Length != frameRange) { InitializeBuffer(); } UpdateBuffer(); CalculateFPS(); }
當buffer滿了以後,又從0開始寫
void UpdateBuffer () { fpsBuffer[fpsBufferIndex++] = (int)(1f / Time.unscaledDeltaTime); if (fpsBufferIndex >= frameRange) { fpsBufferIndex = 0; } }
然後計算平均值
void CalculateFPS () { int sum = 0; for (int i = 0; i < frameRange; i++) { sum += fpsBuffer[i]; } AverageFPS = sum / frameRange; }
除了平均值之外,我們還可以來一個最高值和一個最低值
public int HighestFPS { get; private set; } public int LowestFPS { get; private set; }
然後同樣在Calculate方法中求出來
void CalculateFPS () { int sum = 0; int highest = 0; int lowest = int.MaxValue; for (int i = 0; i < frameRange; i++) { int fps = fpsBuffer[i]; sum += fps; if (fps > highest) { highest = fps; } if (fps < lowest) { lowest = fps; } } AverageFPS = sum / frameRange; HighestFPS = highest; LowestFPS = lowest; }
然後給Display中的欄位賦值顯示出來
public Text highestFPSLabel, averageFPSLabel, lowestFPSLabel; void Update () { highestFPSLabel.text = stringsFrom00To99[Mathf.Clamp(fpsCounter.HighestFPS, 0, 99)]; averageFPSLabel.text = stringsFrom00To99[Mathf.Clamp(fpsCounter.AverageFPS, 0, 99)]; lowestFPSLabel.text = stringsFrom00To99[Mathf.Clamp(fpsCounter.LowestFPS, 0, 99)]; }
在UI中把Panel拉長,然後複製3個text分別顯示最高、平均和最低
6顯示顏色
最後我們讓不同段的FPS顯示不同的顏色。
在Display中創建一個結構體,和這個結構體的數組
這個結構體只會在Display中使用,所以聲明為private。同時添加特性Serializable,讓它在Inspector面板中能看到
[System.Serializable] private struct FPSColor { public Color color; public int minimumFPS; } [SerializeField] private FPSColor[] coloring;
然後我們就可以在Inspector中上顏色啦
我們再優化下代碼,把Display封裝成一個方法
void Update () { Display(highestFPSLabel, fpsCounter.HighestFPS); Display(averageFPSLabel, fpsCounter.AverageFPS); Display(lowestFPSLabel, fpsCounter.LowestFPS); } void Display (Text label, int fps) { label.text = stringsFrom00To99[Mathf.Clamp(fps, 0, 99)]; }
然後根據minimumFPS的值顯示相應的顏色
void Display (Text label, int fps) { label.text = stringsFrom00To99[Mathf.Clamp(fps, 0, 99)]; for (int i = 0; i < coloring.Length; i++) { if (fps >= coloring[i].minimumFPS) { label.color = coloring[i].color; break; } } }
推薦閱讀:
※從項目管理上來說,C++ 是否適合做大的項目?
※學C#需要學好C++么?
※oc和c++混編難度大嗎?
※operator new的問題?
※如何評價Qt Lite Project?