UnrealScript
導言
[編輯]文檔目標
[編輯]這是一份講述UnrealScript語言的技術性文檔。這不是一個教程,也沒有提供有用的UnrealScript代碼的詳細例子。讀者可以參考虛幻引擎,它包了數萬行的UnrealScript原始碼,它解決了諸如人工智慧,運動,inventory,和觸發器等問題。一個好的入手點是看"Actor", "Object", "Controller", "Pawn", and "Weapon" 的腳本。
本文假設讀者具備C/C++ 或Java的編程經驗,熟悉物件導向編程,玩過虛幻遊戲,用過虛幻關卡編輯器(UnrealEd)。
對於OOP(物件導向編程)新手,強烈建議去書店買本Java的書。Java和UnrealScript非常相似,它清晰、簡潔、優秀。
UnrealScript的設計目標
[編輯]由於遊戲編程的自然需要和細微差別UnrealScript被創建出來,為開發團隊和第三方虛幻開發商提供了一個強大的內置程式語言。
UnrealScript的主要設計目標是:
- 支持傳統程式語言沒有解決的時間,狀態,屬性的網絡化。這大大簡化了UnrealScript的代碼。編寫基於人工智慧和遊戲邏輯的程序,主要運用事件交互技術,這些事件採取一定的遊戲時間完成,並且事件非常依賴對象狀態,這讓網絡化問題極度複雜化。在c/c++中書寫、理解、維護和調試這樣的代碼是非常困難的。UnrealScript天生支持時間、狀態、網絡複製大大簡化了遊戲編程。
- 提供Java編程風格,簡單、基於對象的、編譯時錯誤檢查的特性。就象Java為Web程式設計師帶來了清晰、簡潔的編程平台一樣,UnrealScript為3D遊戲程式設計師提供了一個同樣清晰、簡潔、可靠的程式語言。從Java繼承的主要編程概念如下:
- 具有自動垃圾收集的無指針環境;
- 一個簡單的類單一繼承機制;
- 強壯的編譯時類型檢查;
- 安全客戶端執行的「沙盒」;
- 熟悉的c/c++/java代碼外觀和感受;
- 提供遊戲對象豐富的高層互動,而不是底層的位和像素。在設計權衡時,由於UnrealScript工作在對象互動層,而不是底層的位和像素,因此在UnrealScript中,犧牲了性能選擇了簡單性和功能。而在底層和性能的關鍵代碼是用c/c++寫的,在這些地方增加性能獲得的好處超過了增加複雜性得到的壞處,選擇了性能犧牲了簡單性和功能。
在UnrealScript的早期發展期間,我們探索過幾個主要的不同的編程模式。首先,我們研究把Sun和微軟的Java虛擬機的Windows版本,作為虛幻腳本語言的基礎。證明了在虛幻中Java由於缺乏必要的語言特性(如運算符重載),而擁有令人沮喪的限制,同時在巨型圖形對象的情況中,虛擬機任務切換和Java垃圾收集器效率過低,慢得無法接受,Java沒有提供不在c/c++中編程的優點。其次,我們嘗試使用VB的一個早期修改版作為UnrealScript的實現,效果良好,但可惜的是它不符合c/c++程式設計師的習慣。最終,基於對速度和熟悉感的期望,我們決定UnrealScript採用c++/java的一個修改版來實現,同時融入遊戲特有的概念到語言的定義中。
虛幻引擎3的新內容
[編輯]熟悉UnrealScript的讀者們,可以在這裡找到自虛幻引擎2後的重大改變。
- 複製-複製語法在UE3中有以下變化:
- 複製塊現在只用於變量
- 複製函數現在由函數說明符定義(Server, Client, Reliable)
- 堆疊狀態-你現在可以把狀態從堆疊中推進和彈出
- UnrealScript預處理-支持宏和條件編譯
- 調試函數-添加了新的調試函數
- 默認屬性-defaultproperties有所改變和增強
- 默認結構-現在結構也有默認屬性了
- 不允許再給配置或局部變量設置預設值
- Defaultproperties在運行變為只讀,不允許做class'MyClass'.default.variable = 1 操作
- 動態數組-動態數組新增了一個find()方法,用於按索引查詢元素
- 動態數組迭代器-現在能用foreach操作動態數組的方法
- 函數委託-現在UE3允許委託作為函數的參數
- 接口-添加對接口的支持
- 訪問其他類的常量:class'SomeClass'.const.SOMECONST
- 支持多定時器
- 函數默認參數值-現在能指定函數的可選參數了,給可選參數設定默認值即可
- 支持工具提示(Tooltip)-當您的滑鼠懸浮在屬性上時,如果那個屬性的UnrealScript上面存在/** 工具提示文本 */的注釋聲明,那麼編輯器屬性窗口將會顯示一個包含該注釋信息的工具提示。
- 支持元數據-通過與元數據相關聯的各種屬性擴充遊戲和編輯器的功能
程序結構示例
[編輯]這一個典型、簡單的UnrealScript類的例子,它演示了UnrealScript的語法特點。請注意,此代碼可能和當前的虛幻原始碼不一致,因為這個文檔並不參與代碼同步。
//=====================================================================
// TriggerLight.
// A lightsource which can be triggered on or off.
//=====================================================================
class TriggerLight extends Light;
//---------------------------------------------------------------------
// Variables.
var() float ChangeTime; // Time light takes to change from on to off.
var() bool bInitiallyOn; // Whether it's initially on.
var() bool bDelayFullOn; // Delay then go full-on.
var ELightType InitialType; // Initial type of light.
var float InitialBrightness; // Initial brightness.
var float Alpha, Direction;
var actor Trigger;
//---------------------------------------------------------------------
// Engine functions.
// Called at start of gameplay.
function BeginPlay()
{
// Remember initial light type and set new one.
Disable( 'Tick' );
InitialType = LightType;
InitialBrightness = LightBrightness;
if( bInitiallyOn )
{
Alpha = 1.0;
Direction = 1.0;
}
else
{
LightType = LT_None;
Alpha = 0.0;
Direction = -1.0;
}
}
// Called whenever time passes.
function Tick( float DeltaTime )
{
LightType = InitialType;
Alpha += Direction * DeltaTime / ChangeTime;
if( Alpha > 1.0 )
{
Alpha = 1.0;
Disable( 'Tick' );
if( Trigger != None )
Trigger.ResetTrigger();
}
else if( Alpha < 0.0 )
{
Alpha = 0.0;
Disable( 'Tick' );
LightType = LT_None;
if( Trigger != None )
Trigger.ResetTrigger();
}
if( !bDelayFullOn )
LightBrightness = Alpha * InitialBrightness;
else if( (Direction>0 &amp;amp;&amp;amp; Alpha!=1) || Alpha==0 )
LightBrightness = 0;
else
LightBrightness = InitialBrightness;
}
//---------------------------------------------------------------------
// Public states.
// Trigger turns the light on.
state() TriggerTurnsOn
{
function Trigger( actor Other, pawn EventInstigator )
{
Trigger = None;
Direction = 1.0;
Enable( 'Tick' );
}
}
// Trigger turns the light off.
state() TriggerTurnsOff
{
function Trigger( actor Other, pawn EventInstigator )
{
Trigger = None;
Direction = -1.0;
Enable( 'Tick' );
}
}
// Trigger toggles the light.
state() TriggerToggle
{
function Trigger( actor Other, pawn EventInstigator )
{
log("Toggle");
Trigger = Other;
Direction *= -1;
Enable( 'Tick' );
}
}
// Trigger controls the light.
state() TriggerControl
{
function Trigger( actor Other, pawn EventInstigator )
{
Trigger = Other;
if( bInitiallyOn ) Direction = -1.0;
else Direction = 1.0;
Enable( 'Tick' );
}
function UnTrigger( actor Other, pawn EventInstigator )
{
Trigger = Other;
if( bInitiallyOn ) Direction = 1.0;
else Direction = -1.0;
Enable( 'Tick' );
}
}
示例代碼關鍵元素分析:
- 類聲明。每個類"extends"(繼承自)一個父類。每個類屬於一個"package"(包)。包是一個,把多個相關對象組織在一起的對象集合。所有函數和變量屬於一個類,並且只能通過屬於這個類的元件(actor)進行訪問。沒有所謂的全局函數和全局變量。詳見
- 變量聲明。UnrealScript支持非常多的變量類型,包括c/java的基本類型、對象引用、結構和數組。此外,變量還可聲明為讓設計人員無需進行任何編程工作,就能在UnrealEd(虛幻關卡編輯器)訪問的可編輯屬性。這些屬性使用var()語法進行聲明,而不是用var。詳見
- 函數。函數有一個可選的參數列表和返回值。函數可以有局部變量。一些函數由虛幻引擎本身調用(比如BeginPlay)。一些函數由其他地方的腳本代碼調用(比如Trigger)。詳見
- 代碼。支持c和java的標準關鍵字,象for、while、break、switch、if等等。花括號和分號的用法和c、c++和java中的一樣。
- 引用元件(actor)和對象。在腳本中,你能看到一些函數,被另外一個對象通過對象引用調用的情況。詳見
- 「state「(狀態)關鍵字。這個腳本定義了一些「state「,它由函數、變量和代碼構成,這些成分只在元件(actor)進入相應的狀態時才被執行。詳見
- 請注意,在UnrealScript中所有關鍵字、變量名、函數和對象名是不區分大小寫的。在UnrealScript、Demo、demON和demon是相同的東西。
虛幻虛擬機
[編輯]虛幻虛擬機由以下部分組成:伺服器、客戶端、渲染引擎和引擎支持代碼。
虛幻伺服器控制玩家和元件之間的所有互動。在單人遊戲中,客戶端和服務端運行在同一台機器上;網路遊戲的伺服器運行於一台專用機器中;所有玩家使用客戶端鏈連接到這台機器上。
所有玩家都在關卡中進行遊戲,關卡是個包含幾何體和元件的獨立環境。虛幻伺服器有同時運行多個關卡的能力。每個關卡獨立運作互相隔離:元件不能在關卡之間往來,並且一個關卡中的元件不能和另外一個關卡中的元件進行通信。
地圖中的每一個元件可以被玩家(網路遊戲中可以有很多玩家)和腳本控制。腳本可以定義元件如何移動以及如何和其他元件交互。遊戲世界中的元件有序的運作,腳本的執行,事件的發生,等等的一切是如何在UnrealScript中實現的。你可以在下面找到答案:
在時間處理方面,虛幻將每秒的遊戲邏輯劃分為tick做單位的時間片。一個tick是活動對象(actor)被 update的最小時間間隔。在大部分情形下,一個tick 為百分一或者十分一秒tick的劃分細度是依cpu的表現性能的。cpu越強大,計算能力越強,tick就可以劃分得越細。
UnrealScript中的一些命令零tick執行(也就是說,執行它們,遊戲時間沒有任何流逝),而另一些則需要花費許多tick。需要花費遊戲時間的函數稱為「潛伏函數」。Sleep,FinishAnim,和MoveTo都是潛函數的例子。潛伏函數只能在狀態代碼(state code)中調用,不能從函數代碼中調用(包括state函數定義)。
在活動對象的潛伏函數執行完成前,它的state不會繼續執行。而它的非潛伏函數仍然可以被其他活動對象和VM調用。這樣的結果是,可以在任何時間調用UnrealScript函數,即使潛伏函數還未完成。
從傳統編程角度來看UnrealScript的行為,似乎關卡中的每個活動對象都執行在自己的執行緒中。實際上,在UnrealScript內部並沒有使用Windows執行緒,因為那樣效率過低(Window95和WindowNT無法高效的處理數以千計的執行緒並發)。而是採用UnrealScript模擬執行緒。這對UnrealScrit來說是透明得,當你用c++書寫和UnrealScript進行交互的代碼時顯得非常明顯。
所有得UnrealScript腳本是獨立執行得。如果在一個關卡中有100個怪物在周圍走動,所有這些怪物腳本對每一個tick來說是同時獨立得執行得。
筆記:有兩種時間概念:1、現實世界得時間;2、遊戲世界中得時間;現實中得時間一般是用秒來衡量的,也不存在最小時間單位這個概念,因為物理上得時間是很難把握得,我們能感覺時間得流逝,確無法準確得測量它定義它(除非理論物理得到突破,建立大統一理論)。。。。,而遊戲中得時間是由程序定義得,我們從現實中得時間簡化出了遊戲世界時間得概念,在虛幻中遊戲時間得最小單位是tick(相當於現實得10-100毫秒),為了保持遊戲對象得時間同步,UnrealScript使用了模擬執行緒,以保證在定義中得遊戲世界中遊戲對象時間得絕對性質(遊戲對象得狀態變化絕對獨立,每個遊戲對象得時間絕對同時流逝),這在現實中是不可能得。現實中不存在絕對時間得概念,即使通常差別不大。這並不是說幾億個cpu同時執行就能解決同時性的問題,物理本質上就不存在所謂的絕對同時,時間的絕對性是個幻覺。
對象層次結構
[編輯]類
[編輯]每個腳本文件對應一個類,以class聲明為開頭,後面是類的父類和類的相關信息。簡單的例子如下:
class MyClass extends MyParentClass;
這裡聲明了一個名稱為「MyClass」的新類,這個類繼承了「MyParentClass「的所有功能。此外類駐留在名為」MyPackage「的包中。
每個類繼承父類的所有變量和函數以及狀態。每個類都可以申明新的變量和新的函數(或者重寫已經存在的函數)以及添加新的狀態(或為已有的狀態添加功能)。
在UnrealScript中設計一個新類(例如一個牛頭人怪物)的典型的做法是,從具備你需要的功能的已有類(例如,Pawn類,所有怪物的基類)繼承。
通過這樣的做法,你永遠都不用重新發明輪子--可以簡單的添加新功能,你可以在自定義的同時保持不需要定製的現有功能。這種做法特別適合從虛幻中繼承AI,內置的AI系統提供了大量的基本功能,同時你還可以建立自己的生物。
類聲明有以下可選的說明符:
Native(包名稱)(本機類)
指示:「這個類使用幕後c++支持「。虛幻native類包含在一個c++寫的exe中。只有native類可以申明native函數或實現native接口。native始終來自native類。與腳本變量和特定的函數協作,native類需要創建一個自動生成的C + +頭文件。默認情況下,包名是腳本類所在的exe名稱。例如,引擎包中的類,要求EngineClasses.h文件。
NativeReplication
表示這個類的變量複製處理在c++中實現。只能用於native類。
DependsOn(ClassName[,ClassName,....])(依賴於)
表示ClassName是在這個類前編譯。ClassName必須和這個類處於同一個包或者前一個包。可以用一個DependsOn指定多個依賴類,他們用逗號隔開。也可以使用多個DependsOn指定。
Abstract
聲明類是一個「基本的抽象類」。這可以防止這個類被無意的實例化。這個關鍵字會對其內部子類起作用,但是不會對其他腳本文件中的子類起作用。
Deprecated(過時的)
類的所有對象被加載但不保存。當關卡設計師在關卡設計器中載入包含過時活動對象被的地圖時,他們會收到編輯器的警告信息。這個關鍵字會對其子類產生作用。
Transient(瞬態)
表示該類的對象不應該被保存在磁碟上,這和一些非持久性的本機類自然結合是有用的。比如播放器和窗口。這個關鍵字會對其子類起作用;子類能使用NotTransient關鍵字重寫它。
NotTransient(非瞬態)
表示不從基類繼承Transient關鍵字標註的瞬態特性。
Config(IniName)(配置)
表明這個類允許訪問ini文件中的數據。如果在類中有可配置變量(使用config和globalconfig申明),這可以讓類訪問那些保存在指定的配置文件的變量。這個標誌將被傳播到所有子類並且不能被否定,但是子類可以通過config關鍵字指定不同的配置文件。通常IniName指定ini文件的名稱去存儲數據,但有些名字具有特殊意義:
- Config(Engine):用於引擎配置文件,通過你的遊戲名稱追加"Engine.ini"。例如,ExampleGame遊戲的引擎配置文件名稱是"ExampleEngine.ini"。
- Config(Editor):用於編輯器配置文件。通過你的遊戲名稱追加「Editor.ini"。例如,ExampleGame遊戲的編輯器配置文件名稱是"ExampleEditor.ini"。
- Config(Game): 用於遊戲配置文件。通過你的遊戲名稱追加"Game.ini"。例如,ExampleGame遊戲的配置文件名稱是 ExampleGame.ini。
- Config(Input):用於輸入配置文件。通過你的遊戲名稱追加「Input.ini「。例如,ExampleGame遊戲的輸入配置文件名稱是ExampleInput.ini。
- PerObjectConfig:類的每個對象的配置信息都會被保存在配置文件中。每個對象在配置文件中都有一個如下格式的配置節[ObjectName ClassName]。這個關鍵字會對其子類起作用。
PerObjectLocalized
類得本地化數據將被定義在每對象得基礎上。每個對象在已命名得本地化文件中有個如下格式得節[ObjectName ClassName]。這個關鍵字會對其子類起作用。
EditInlineNew
編輯器,表示這個類的對象能從虛幻便捷器的屬性窗口創建(默認行為是可以通過關聯的屬性窗口引用已經存在的對象)。這個關鍵字會對其子類起作用。子類可以使用NotEditInlineNew關鍵字重寫它。
NotEditInlineNew
編輯器,不從基類繼承EditInlineNew關鍵字。如果沒有任何父類使用EditInlineNew關鍵字將不起效果。
Placeable(可放置)
編輯器。表示這個類可以通過虛幻編輯器放到關卡,UI場景,或者kismet窗口(由類的類型定)。這個標誌將被傳播到其所有子類中;子類能通過NotPlaceable關鍵字重寫它。
NotPlaceable
編輯器。不從基類繼承NotPlaceable關鍵字。表示這個類不能在虛幻編輯器中放入關卡。
HideDropDown
編輯器。防止這個類在虛幻編輯器的屬性窗口的組合框中顯示。
HideCategories(Category[,Gategory,......])
編輯器。表示這個類的對象在虛幻編輯器的屬性窗口中隱藏一個或多個類別。使用無類別聲明可以隱藏變量,使用類名稱聲明變量的名稱。
ShowCategories(Category[,Category,......])
編輯器。不從基類繼承ShowCategories關鍵字。
AutoExpandCategories(Category[,Category,......])
編輯器。指示一個或多個類別在編輯器的屬性窗口中自動展開。使用無類別聲明可以自動展開變量,使用類名稱聲明變量的名稱。
Collapsecategories
編輯器。表示類的屬性不應該在虛幻編輯器的屬性窗口類別中被分組。這個關鍵字會對其子類起作用;子類可以用DontCollapsecategories關鍵字重寫它。
DontCollapsecategories
編輯器。表示不從基類繼承Collapsecategories。
Within ClassName
高級。表示類的對象離不開ClassName的實例。為了建立這個類的對象,您必須指定作為外部對象ClassName的實例。這個關鍵字必須作為class申明的後的第一關鍵字。
Inherits(ClassName[,ClassName,......])
高級,使用多繼承。只是適用於native類。不支持從多個UObject類繼承。
Implements(ClassName[,ClassName,......])(實現)
高級。指示類實現的多個接口。只有nataive類可以實現native接口。
NoExport
高級。表明這個類的c++聲明不應該通過腳本編譯器包含在自動生成c++頭文件中。類的c++聲明手動定義在一個單獨的頭文件中。只適用於native類。
變量
[編輯]變量類型
[編輯]內置類型 (Built-in types)
[編輯]這裡有一些在 UnrealScript 中宣告變數的例子:
var int a; // 宣告一個名稱為"A"的整數變數。
var byte Table[64]; // 宣告一個長度64的靜態 1-byte 陣列。
var string PlayerName; // 宣告一個名稱為"PlayerName"的字串變數。
var actor Other; // 實體化 Actor 類別,並命名為"Other"。
var() float MaxTargetDist; // 宣告一個名稱為"MaxTargetDist"的浮點數變數,並且它的值可以從 UnrealEd 的屬性窗口中修改。
變數在 UnrealScript 能出現在兩種地方:實體變數,它可以在整個類別內使用,在宣告完類別或 struct 後立即出現。局部變數,出現在函數中,只有在函數執行時有效。實體變數使用關鍵字 var 宣告。局部變數用關鍵字 local 宣告,例如:
function int Foo()
{
local int Count; Count = 1; return Count;
}
以下是 UnrealScript 中支持的內置變數類型:
byte: 1-byte 值,範圍從 0 到 255。
int: 32-bit 整數值。
bool: 布林運算值: 不是 true 就是 false。
float: 32-bit 浮點數值。
string: 一個字串(see Unreal Strings)。
constant: 不能修改的變數。
enumeration: 一個能夠代表多個預先設定的整數值中的其中一個值。例如:在 Actor 內定義為 enumeration 的變數 ELightType 描述了一個動態光源,它的值可以像 LT_None、LT_Pulse、LT_Strobe,等等...。
聚合類型(Aggregate data types)
[編輯]虛幻類型
[編輯]變量說明符(specifiers)
[編輯]變量可編輯機制(Editability)
[編輯]數組
[編輯]結構
[編輯]結構說明符(specifiers)
[編輯]枚舉
[編輯]常量
[編輯]對象和元件(actor)引用變量
[編輯]類引用變量
[編輯]表達式
[編輯]賦值
[編輯]類之間轉換對象引用
[編輯]函數
[編輯]函數聲明
[編輯]函數參數說明符(specifiers)
[編輯]函數重載(Function Overriding)
[編輯]"函數重載"是指在子類別中寫一個同名的新函數。例如:你正在為一個名叫 Demon 的新怪物寫一個類別。這個 Demon 類別,是繼承自 Pawn 類別。現在,當怪物重生第一次看到玩家時,會呼叫父類別 Pawn 的 SeePlayer 函數,所以這個怪物會開始攻擊這個玩家。這是一個很好的概念,但是當你想在你的新的 Demon 類別中以不同的方式處理 SeePlayer 函數時,你該怎麼實現?答案就是函數重載。
重載一個函數,只需要從父類別中將函數的定剪下並複製貼上到你的新類別中。例如:你可以加入SeePlayer 函數到你的新類別 Demo 中。
// New Demon class version of the Touch function.
function SeePlayer( actor SeenPlayer )
{
log( "The demon saw a player" ); // Add new custom functionality here...
}
函數重載是創造新 UnrealScript 類別的關鍵。你可以創造一個繼承自現有類別的新類別。然後,你需要做的事是重載一個你需要進行不同做法的函數。這可以使你在不必寫大量程式碼的情況下就建造一個新的類別。
在 UnrealScript 中的某些函數以 final 關鍵字做定義。這個 final 關鍵字(在關鍵字 function 馬上出現的修飾字)意思是"這個函數無法被子類別所重載"。這可以用在一個你知道沒有人會去重載的函數中,因為它會有更快的代碼執行速度。例如:假如你有一個稱做 VectorSize 用來計算一個向量大小的函數。可以確定的是絕對沒有任何原因來讓任何人重載它,所以可以在 function 後馬上加入關鍵字 final。另一方面,像 Touch 這類的函數是很注重情境的,所以不能宣告它為 final。