五、Unity C#編程
遊戲運行模式
- 程序首先初始化
- 然後進入一個while(true)循環 檢查是否有消息(包括鼠標事件等)
若有消息 則處理後 然後計算 繪製場景
程序處在這麼一個大循環中 不斷檢查是否有事件 若有則處理
幀頻
在while循環中 遊戲會有一秒循環的次數 比如CPU可以一秒繪製80次畫面
人對於畫面的流暢感若到了60 其實已經非常流暢了
幀頻若達到60 則可以不用繼續提升了 若繼續提升 其實也感覺不出來 而且會更加消耗CPU
因此 在繪製的時候可以看時間是否到達 若還沒到 則sleep
1/60=0.0166秒 但比如只有0.01秒就全部處理完了 那麼可以休眠0.0066秒 休眠是爲了節約CPU
因此 在while中 有:
- 事件處理(包括各種事件)
- 繪製場景
- 檢測是否需要休眠(維持幀頻在60左右)
若CPU比較低端 那麼繪製速度會變慢 此時while會不斷地繪製 就不會循環了
FPS
FPS有兩個概念
- 1、幀頻 (Frames Per Second)
- 2、第一人稱射擊 (First Person Shoot)
🚩組件的代碼入口
每個節點都有多個組件
因此 組件是經常面對的開發模式
- 當組件被掛載到節點的時候 會調用組件的一個函數:Awake
- 當節點在while循環裏 刷新前 會調用Start
- 當節點在while循環裏 要處理的時候 會調用Update
每個while循環要處理的時候都會調用每個組件的Update
模塊化開發 & 代碼模塊
實際上 組件成了很多入口的模塊
因此 其實是根據Unity邏輯來開發模塊
給Unity寫代碼 實際上是給Unity寫代碼模塊
開發
先在Project的scripts裏右鍵 -> Create -> C# Script 以創建一個C#代碼塊
此時的代碼是一個組件 組件只有掛載到節點上纔會在Unity的while裏循環被調用
在Hierarchy右鍵 -> Create Empty 創建一個根節點
點擊Add Component添加組件
點擊Scripts 然後選擇腳本即可:
雙擊Project裏的腳本圖標 即可打開Visual Studio編輯器
自動生成的代碼:
using UnityEngine;
using System.Collections;
public class game_scene : MonoBehaviour {
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
}
}
改一下:
using UnityEngine;
using System.Collections;
// game_scene組件類繼承於Unity提供的基類MonoBehaviour
public class game_scene : MonoBehaviour {
// 組件實例加載的時候調用
void Awake()
{
}
// Use this for initialization
// 組件實例在第一次Update之前調用
void Start () {
}
// Update is called once per frame
// 遊戲每次刷新的時候調用
void Update () {
}
// 物理引擎每次固定刷新的時候調用(與幀頻無關)
// 主要用於物理計算
void FixedUpdate()
{
}
}
其中 MonoBehaviour就是Unity的代碼模塊/組件 是組件的基本規則
public class game_scene : MonoBehaviour
game_scene : MonoBehaviour代表game_scene繼承/擴展自MonoBehaviour 所有組件都必須基層與MonoBehavior 必須遵守這個規則
在一個腳本里有且只能有一個類繼承自MonoBehavior 且該類名必須與腳本文件名保持一致
Awake Start Update 和 FixedUpdate都是很重要的接口 他們是在Unity上開發組件代碼的入口
(也類似於其它編程語言或框架的生命週期)
FixedUpdate是Unity提供的固定的機制
Update幀頻是隨時在變的 是浮動的 是實時的 只是維持在60上下
而FixedUpdate是以固定的頻率來調用的 根據當前CPU的幀頻給出一個固定的頻率
在繼承了MonoBehaviour之後 就具備了MonoBehaviour的所有特性
正因如此 當game_scene組件實例化之後加到節點上 Unity的while循環才能調用到諸如Awake Start Update 和 FixedUpdate的基本函數
還有個OnGUI接口
Unity提供了一種GUI(Graphic User Interface 界面)元素的繪製機制 這就是OnGUI
比如在遊戲裏要顯示暱稱等2D文字 Unity提供OnGUI 當繪製3D物體了 要將其變爲2D成像 然後會調用OnGUI接口 此時 即可繪製GUI元素
並不是生成一個GUI節點 而是繪製(draw)出GUI元素
OnGUI在每次的刷新(Update)的時候都會被調用
// 繪製2D元素的入口的時候調用 例如玩家的暱稱和血量條
voidOnGUI()
{
}
一個組件可以掛載多個腳本
使用Debug.Log()
打印Debug日誌輸出語句
組件實例化
定義了類只是一個描述 而並不是一個實例 class只是組件的類型
要將類創建爲實例纔行
掛載的並不是類的本身 而是該類的類型的實例 因此 掛載多個並不會衝突
在添加組件的時候 創建了該組件類的對象實例
然後在gameobject對象中保存了該組件的實例
🚩Unity C#基本數據類型
由於是Unity C# 所以和C#其實還是有一些細微差別的
程序包含數據和代碼 數據是在運行過程中產生的
這些都是存放在內存中的
內存存儲的最小單位是字節
1字節=8比特(bit)
- 整數 / 1字節
- sbyte 帶符號的整數 / 1字節 (需要多拿出1bit來表示符號位)
- byte 不帶符號的整數 / 1字節
- short 帶符號的整數 / 2字節
- ushort 不帶符號的整數 / 2字節
- int 帶符號的整數 / 4字節
- uint 不帶符號的整數 / 4字節
- long 帶符號的整數 / 8字節
- ulong 不帶符號的整數 / 8字節
- 浮點數
- float / 4字節
- double / 8字節
- 邏輯
- true
- false
- 字符 / 2字節(16位Unicode字符)
- 複雜類型的引用變量(用於表示一個變量 指向另一個複雜的對象 保存着對該變量的引用)
- 若在64位的.net那就是64bit/8字節
- 若在32位的.net那就是32bit/4字節
- String字符串也是一個複雜類型
- 類可能有多個成員 因此叫做複雜對象
Unity C#權限修飾符
- public 類及類型成員的修飾符
- private 類型成員的修飾符(默認)
- protected 類型成員的修飾符
- internal 類及類型成員的修飾符(默認)
用public修飾的類可以在外部使用 用internal修飾的類只能在類的內部使用
類型成員分爲兩個:一個是數據成員(不屬於類 只屬於類的實例) 另一個是類的方法成員
類的實例會擁有數據成員 而每個實例的數據成員不一定相同 但是方法成員必定都是相同的 因爲都是由這個類所產生的
比如 都是人 都會走路跑跳 但是每個人的頭髮或者身體都是其自己的
// 定義一個類
publicclass Person
{
// 數據成員的定義
private string name;
private int age;
private int gender;
// 成員函數的定義
public string eat(int age)
{
return "asd";
}
}
類的實例化
類也需要實例化
類的實例就是類的數據成員所需要的內存體
C#內存模型
C#的內存模型裏面共有四塊區域
分別是:
- 代碼段 用於存放函數指令和常量字符串 所有實例共用
- 還有個數據段 用於存放全局變量(static靜態變量)
- 還有個堆 用於存放
new
出來的複雜對象 當沒有任何一個引用變量指向該new出的內存時即被回收 - 還有個棧 用於存放局部變量 函數返回變量即被回收
堆和棧會在一定情況下被回收 代碼段和數據段是常駐內存不回收的
堆和棧中的是程序運行纔會產生的 代碼段和數據段是程序一加載首先加載的
out關鍵字
被out
修飾的參數 在函數內可直接修改該變量的值
out可以理解爲將修改後的值帶出來 有些類似於引用和指針的概念
由於是在函數裏另外new了一個對象 因此地址是不同的 帶出來的是在函數內new出來的對象
但若不加out的話 即使在函數內將傳入的對象改了 那麼在外面的也不會受影響
這就是加不加out的區別
Person p=new Person();
p.age=10;
create_person(out p);
Debug.Log(p.age); // 12
void create_person(out Person p)
{
p=new Person();
p.age=12;
}
繼承
在C#中 使用:
來繼承
比如 Son繼承Parent
public class Son : Parent
{
}
在繼承的時候 若基類/父類爲internal 那麼子類也必須爲internal
若基類/父類爲public 那麼子類可以爲public 也可以爲internal
調用順序
this.xxx()調用的是自己的函數
繼承後 子類成員函數調用時的查找方式是先從當前類中找
若找到則調用自己的 若未找到 則往基類找 直到找到爲止
base關鍵字
若當前類和基類有同名函數 那麼base.xxx()
調用的是基類的函數
base只能在類的內部使用
虛函數
爲同時管理多個成員 提出了虛函數的概念
- 基類的引用變量保存子類的實例
- 爲方便管理 需在基類定義幾個接口以統一管理
基類定義幾個函數接口 子類自己重載 然後有不同的實現
子類繼承了基類 若調用方法 那麼會調用子類的該方法
此時 若想要調用父類的該同名方法 那麼可以用虛函數(virtual
關鍵字)
public virtual void sayHello()
{
Debug.Log("Hello");
}
virtual表示該函數爲一個虛函數 若遇到該函數爲虛函數 那麼會去基於該實例查找基類是否爲virtual
若爲virtual 則會查找子類是否重載該虛函數
此時 還需要在子類的函數上用override
關鍵字顯式地定義重寫 代表重寫了基類的同名虛函數
public override void sayHello()
{
Debug.Log("Hello World");
}
若有 則調用子類的函數
——“我不知道外界會傳入什麼樣的實例 但我依然能夠分情況調用不同的函數 執行各自的邏輯處理”
🚩const & readonly
const常量
const常量全局唯一 只有一個
const修飾的是類的成員變量
它是在編譯的時候就確定的常量
readonly只讀
每個實例都會有一個readonly只讀變量
它是在實例化的時候確定的常量
readonly的變量只有一次修改的機會 在對象構造的時候 在構造函數裏修改
🚩名稱空間
在代碼中有可能會使用同樣的名字
若名字出現重複 則會產生衝突
因此 要使用名稱空間 名稱空間帶有自己的烙印
代碼全都寫到該名稱空間中 這樣 及時代碼中出現了同樣的名稱 但名稱空間不同 因此不會出現衝突
用namespace
關鍵字來定義名稱空間
namespace my_namespace
{
class Person
{
int gender;
}
}
my_namespace.Person p = new my_namespace.Person();
簡寫 / 省略名稱空間
每次前面都要加上名稱空間 過於麻煩
此時 可以using 名稱空間
往搜索範圍中添加該名稱空間 然後使用時即可省略名稱空間了
流程:首先去當前名稱空間查找 若找不到 去using的名稱空間裏查找 直至找着爲止
在using的全部名稱空間裏都找不到 則會報錯
// 往搜索範圍中添加該名稱空間
using my_namespace;
namespace my_namespace
{
class Person
{
int gender;
}
}
Person p = new Person();