基礎5——FPS——對性能的測量

匯總

雪之下雪乃:純文字和圖片的Unity教程-系列概述和匯總zhuanlan.zhihu.com圖標純文字和圖片的Unity教程-系列概述和匯總雪之下雪乃:純文字和圖片的Unity教程-系列概述和匯總zhuanlan.zhihu.com圖標

本節將會涉及

  1. 利用Unity的物理特性創建不斷增大的原子核
  2. 利用Unity的profiler來分析性能
  3. 測量和展示幀率
  4. 避免創建臨時字元串
  5. 穩定幀率
  6. 給幀率的展示上顏色

我們在這節中將會創建一個簡單的測試場景並測量幀率。我們先用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?

TAG:C | Unity遊戲引擎 | 遊戲開發 |