SCAR–Scripting at Relic

1.SCAR的第一課
2.事件表
 2.1 建立事件
3 特殊函式
4.建立任務
 4.1建立戰役結構
  4.1.1 建立 .level 檔案
 4.2 建立 .lua 檔案
  4.2.1 範例Mission.lua script
5.設定 .CAMPAIGN 檔案
6.雜項檔案
 6.1 .DAT 檔案
  6.1.1 將你的任務方言化(Localize)
 6.2 Teamcolour.lua
 6.3 AI.lua
 6.4 ReferenceFleet.lua
 6.5 Datfiles.lua
 6.6 Mission.tga
7.附錄
8.聲明

1.SCAR的第一課

SCAR(SCript At Relic)是一種線性的腳本語言(Script Language),用來建構一個規則導向(Rule Based)的系統,類似ICPC所使用的觸發導向(Trigger Based)系統.SCAR是以LUA這種 script 語言為基礎,包含了LUA大部分的內建功能.程式設計師透過一些函式來提供一個系統,用以存取遊戲中特定項目的狀態-例如下指令-並且修改這些項目的狀態.

規則(Rule)是一個使用者定義的函式,一但加入後,遊戲會每隔一段間隔作一次評估的動作.加入規則的方式是利用Rule_Add("name of Rule")函式,藉以傳遞你要加入的規則的名稱.每次評估規則的間隔時間預設值是一個frame的大小,也可以在使用Rule_Add的地方使用Rule_AddInterval函式來自訂間隔時間為幾秒.你可以給每個規則可以各自不同的評估時間間隔.

像是若我們想要加入一個每個frame都會在控制台(console)印出"Hello world!"字樣的規則,我們可能會有下列這幾行程式:

-- 將 Rule_HelloWorld 加入要被評估的規則的序列之中
Rule_Add("Rule_HelloWorld" )

-- 在這裡,Rule_HelloWorld 函式以符合LUA標準的方式被定義
function Rule_HelloWorld()
    print("Hello world!")
end

如以一來,每個frame都會在主控台印出「Hello world!」字樣

一但規則被加入,遊戲每隔一個間隔就會執行一次此規則.如果此規則的第一行敘述是條件敘述,那就能確保這個規則只有在條件適當的時候才會被執行.

讓我們利用條件敘述,讓「Hello world!」只有在玩家是西加拉人時才會被印出:

Rule_Add("Rule_HelloWorld")

function Rule_HelloWorld()
    -- 利用 Player_GetRace 函式來判定Player 0是不是Hiigaran
    if ( Player_GetRace( 0 ) == Race_Hiigaran ) then
        print("Hello world!")
        Rule_Remove("Rule_HelloWorld")
    end
end

注意我們在這裡呼叫了 Rule_Remove .這會把規則從序列中移除,所以下一個間隔之後這個規則就不會被執行.如果你不再規則被執行後將之移除,那他將會繼續在每個時間間隔過評估一次.藉由 Rule_Remove 我們可以確保規則只有在條件符合的時候被執行一次.

規則可以在你的程式的任何地方被移除,但是一直要到下一個間隔過後才會真正的被移出.如果你再規則的中間加上一除規則的敘述,規則剩下的部分還是會被執行,直到下個間隔為止.

看起來好像沒什麼不好,不過要是玩家的ID並不是0呢?或者在多人遊戲中我們希望能夠檢查每個玩家的話要怎麼做?我們可以把敘述包入一個 for 迴圈中,於是就會跑過每一個玩家來檢查玩家是不是西加拉人.

function Rule_HelloWorld()
     -- 我們使用for迴圈,一個Lua所提供的功能
    for i=1,Universe_PlayerCount() do
        if ( Player_GetRace( i ) == Race_Hiigaran ) then
             print("Hello world!")
         end
         -- 所有的玩家都檢查過了以後,移除這個規則
         if ( i == Universe_PlayerCount() ) then
             Rule_Remove("Rule_HelloWorld")
         end
     end
end

在這個規則中, for 迴圈會檢查遊戲中每個玩家,看看是不是西加拉人,是的話就印出「Hello world!」.當迴圈的 i 的值等於玩家的數目時,這條規則就會被移除

看到了這個簡單的規則很快的變的非常有用了嗎?這就是SCAR內藏的力量.在最基礎的層級上也能相當有用.而藉著更進階的Script技巧,它可以成為非常強大而且多用途的工具.

如同你所見的,規則只是一個函式.讓他成為規則的就只是我們在前面加上 Rule_ (這只是為了辨識方便),以及我們將他加到規則列表中被評估,而不是直接從script中呼叫.這表示你也可以在你的script中創造一些簡單的函式來作重複的動作,而不用建立一堆執行相同功能的規則.

2.事件表

SCAR的另外一個功能是事件表(Event Table).事件表是一個項目清單,這些項目發生時會持續一段時間,事件表記載何時從某一段(segment)跳到下一段.他的流程是完全直線的,但是幾乎可以包含任何可以從規則中去呼叫的東西.僅有的例外是條件判斷式(像是 if)或是迴圈(像是 for).

事件最典型的應用IntelEvent或Autofocus,或是其他你希望一連串的事情在你的控制之下一個接一個發生的場合.下面是一個事件的實例.

-- 建立事件表
Events = {} -- 表格的名稱必須是Events,遊戲會去查這個表格
-- 這裡我們為事件取名字
Events.intelevent_constructinterceptors =
{
  {
    -- 這是事件的第一段
    -- 在這裡我們作一些基本的設定,啟動 universe skipping,打開 letterbox
    { "Universe_EnableSkip(1)", "" },
    { "Sound_EnterIntelEvent()","" },
    { "Sound_SetMuteActor('Fleet')", ""},
    HW2_Letterbox( 1 ),
    HW2_Wait(2),
  },
  {
    -- 在感應器管理介面中以fighterproduction為中心,產生一個圓圈
    { "Sensors_EnableCameraZoom( 0 )","" },
    { "Sensors_Toggle( 0 )", "" },
    { "g_pointer_default = HW2_CreateEventPointerSubSystem( 'FighterProduction', 'Mothership' )","" },
    { "Camera_Interpolate( 'here', 'camera_focusOnFighterSub', 2)",""},
    HW2_SubTitleEvent( Actor_FleetCommand, "$40550", 5 ),
  },
  {
    -- 艦隊指揮又來說話了!
    HW2_SubTitleEvent( Actor_FleetCommand, "$40551", 5 ),
  },
  {
    HW2_Wait(1),
  },
  {
    -- 把圓圈移除,建立一個主要任務目標
    { "EventPointer_Remove(g_pointer_default)", "" },
    { "obj_prim_buildtwointerceptors_id = Objective_Add( obj_prim_buildtwointerceptors, OT_Primary )", "" },
    { "Objective_AddDescription( obj_prim_buildtwointerceptors_id, '$40965')", "" },
    { "Objective_AddDescription( obj_prim_buildtwointerceptors_id, '$40966')", "" },
    { "Player_UnrestrictBuildOption( g_playerID, 'Hgn_Interceptor' )",""},
    HW2_SubTitleEvent( Actor_FleetIntel, "$40552", 5 ),
  },
  {
    -- 停掉 letterbox,清除這個事件
    HW2_Letterbox( 0 ),
    HW2_Wait(2),
    { "Sound_SetMuteActor('')", ""},
    { "Sound_ExitIntelEvent()","" },
    { "Sensors_EnableCameraZoom( 1 )","" },
    { "Universe_EnableSkip(0)", "" },
  },
}

事件是藉由 Event_Start( ) 函式來呼叫. Event_Start 函式會傳遞你想呼叫的事件.然後你呼叫的事件就會從頭開始執行到尾.讓我們來看這個如何和我們的 HelloWorld 規則一起運作.我們來把沒什麼用處的列印敘述改成呼叫上面這個事件.

Rule_Add("Rule_HelloWorld")

function Rule_HelloWorld()
    -- we use the for loop, an functionality provided by Lua
    for i=1,Universe_PlayerCount() do
        if ( Player_GetRace( i ) == Race_Hiigaran ) then
            -- instead of printing Hello World, start the event
            Event_Start("intelevent_constructinterceptors")
        end
         -- 所有的玩家都檢查過了以後,移除這個規則
         if ( i == Universe_PlayerCount() ) then
             Rule_Remove("Rule_HelloWorld")
         end
     end
end

事件看起來是有點複雜,不過其實是很直覺的.讓我們把事件拆成幾個部分來看,以對整個架構有更深一步的認識.

2.1建立事件
第一件要作的事情是把事件表格初始化.方法是:

Events = {}

一定要在你的事件開始時去作呼叫,只會呼叫一次,而且名字一定要叫做"Events".因為遊戲會去查這個名字的表格.如果表格不是這個名字的話,你所有的事件都沒辦法運作.所以當你遇到問題時,記得檢查有沒有漏了這一行.

接下來是為你的事件取名.也就是是下面這一行:

Events.intelevent_constructinterceptors =

於是我們有了一個稱作"intelevent_constructinterceptors"的事件.等下我們會用這個名字來開始執行事件以及檢查事件是否已經完畢.因此事件的名字必須是獨一無二的,不能和其他事件重複.

接下來的就是事件的本體了,呼叫函試的區塊會決定接下來會發生什麼事情.如妥我們可以看到的,事件表被拆成好幾段.每一段都用大括號 { 和 } 包起來.每個在相同括號裡面的呼叫都會同時被執行.所以接下來的:

}
    {"Universe_EnableSkip(1)", "" },
    {"Sound_EnterIntelEvent()","" },
    {"Sound_SetMuteActor('Fleet')", ""},
    HW2_Letterbox( 1 ),
    HW2_Wait(2),
},

會同時啟動skipping,告訴聲音部分我們進入了一個Intelevent,讓艦隊指揮停止說話,打開Letterbox,並且等待2秒鐘.關鍵在於HW2_Wait( 2 ).這相當於一個控制器,控制我們何時會進入到事件列表的下一個段落.以這裡來說是兩秒鐘後.

事件被分成好幾個分明的段落.所以我們可以在某個特定的段落中暫停一段時間,讓這個段落能在進入下個段落前跑完.

如同你所見,段落的內容不過就是函式的呼叫而已.注意到 Universe_EnableSkip(1) 和 HW2_Wait(2) 之間的不同.前者被包在大括號裡面,後面還有一個空的欄位,而後者是函式的呼叫.

這兩者之間的差別是,HW2_Wait 是一個helper函式會回傳下列的值:

    {"wID = Wait_Start( 2 )","Wait_End( wID )"},

注意到第二個元素是如何去包含其他的函式呼叫了嗎? The second element specifies the function that, when it returns true, ensures this Controller will step to the next segment of the Event list.

在上面的例子中,第一個項目是呼叫 Wait_Start ,這會回傳一個唯一的ID,第二個項目以這個 ID 作為參數呼叫 Wait_End 函式.當指定的時間經過之後, Wait_End 會傳回 ture,讓這個控制器去允許進入下一個段落.

如果第二個項目是空的,那表示第一個函式只會單純的被執行,而不會作用為一個控制器.

所以,若是想要讓這個段落中間不包含helper函式,那看起來就會像這樣:

{
    {"Universe_EnableSkip(1)",""},
    {"Sound_EnterIntelEvent()",""},
    {"Sound_SetMuteActor('Fleet')", ""},
    {"Camera_SetLetterboxStateNoUI( 1, 2 )",""},
    {"wID = Wait_Start( 2 )","Wait_End( wID )"},
},

3.特殊函式

如果你已經看過萬艦二的關卡的SCAR script,你會注意到一些共通之處.像是 OnInit( ) 函式.

OnInit( ) 函式會在關卡第一次被起動時執行,通常包含了加入 Rule_Init( ) 的呼叫. OnInit( ) 和 Rule_Init( ) 之間的差別在於,當關卡被遊戲載入時,遊戲會自動尋找 OnInit( ). 這是遊戲用來開始關卡的進入點.如果 OnInit( ) 裡面沒有東西,那你的任務什麼也不會做!

注意 OnInit( ) 函式跟 OnStartOrLoad( ) 函式的差別. OnInit( ) 只會在關卡第一次被載入時執行,如果這個關卡是因為讀取遊戲存檔而被載入,那麼 OnInit( ) 不會被執行.這就是為什麼要有 OnStartOrLoad( ).如果你有每次載入時-甚至從遊戲存檔中-都需要去啟動的東西(例如某些需要不斷重複的特效),把需要用到的呼叫放在OnStartOrLoad( ) 函式裡面.

4.建立任務

這裡我們會概略描述為萬艦齊發二建立任務的基本步驟.首先我們要為這個教學建立一個新的戰役.然後為這個戰役建立一個基本的任務.

一但目錄結構已經正確的被建立好後,建立任務需要三個步驟.第一,建立並且匯出 mission.level 檔,第二,建立一個基本的 .lua 檔案,第三,將這個任務加進 .campaign 檔裡面.

為了要執行這些步驟,你會需要:

  • 安裝MAYA,並且正確的安裝了Relic的工具組.
  • 安裝萬艦二(你會需要的是source data,而不是.big檔案)

4.1 建立戰役結構
首先我們要建立一個資料夾,用來存放我們的新戰役.

一個戰役中的所有任務都會存放在 data\LevelData\Campaign 資料夾中.在這個資料夾裡面有許多的子目錄,每一個都存放了更多的子目錄,其中存放著真正的任務檔案本體.我們現在要建立一個"Postmortem"(分析用)戰役.

首先,在 data\LevelData\Campaign 資料夾下面建立一個"Postmortem"資料夾,這是我們新戰役的名稱(是沒什麼想像力,不過很適合我們的目的).全部的任務都會放在這裡面.

你可能已經注意到在 Campaign (戰役)資料夾裡面有幾個 .campaign 檔.他們定義了在戰役中會進行哪些任務,以及其他像是過場動畫之類的東西.我們等下在來看這些檔案.

4.1.1 建立 .level 檔案
既然我們已經開始建立戰役的結構,我們會需要建立一些真正的.level檔案給任務.要建立 .level 檔案,開啟Maya,然後開啟 LevelEd 工具. LevelEd 工具的細部探討會在另外一份教學中提到.為了你的關卡能夠正確的工作,會需要這些基本元素:

  • 如果這一關的艦隊是來自上一關,那你會需要一個玩家開始點.如果不是的話,則需要一艘屬於Player0 的母艦.(要取得有關於如何建立以及使用固有艦隊(persistent fleet,指艦隊來自上一關剩下的兵力),請看"Reactive Fleet postmortem")(譯註:RDN釋出的文件裡面並沒有這一份).
  • 敵對的兵力(任何不屬於 Player0 或是 Galaxy 的船隻)
  • 在地圖上放置資源.
  • 為所有你建立的玩家設定 Race ID.
  • 設定任務的背景

由於我們正要建立的任務是我們的戰役的第一關,在你的 .level 檔案裡面放入這些項目:

  • 一艘屬於 Player0 的西亞加拉母艦,並且屬於"Player_Mothership"這個 SobGroup.
  • 一艘屬於 Player1 的維格航空母艦,屬於"AI1_Carrier"這個SobGroup
  • 將 Player0 的種族設為西亞加拉,而 Player1 的種族設為維格.
  • 將背景設為M01.

一但你已經把這些項目都放到你的地圖中,在萬艦二目錄下建立 "datasrc\LevelData\Campaign\Postmortem\Mission_01"資料夾,並且把MAYA產生的 .ma 檔案存放在 Mission_01 資料夾中,而且檔名要和資料夾相同(也就是 Mission_01.ma )

接下來,把這個檔案匯出到你的"Postmortem"戰役資料夾中的 Mission_01 目錄,並且把 .level 檔案取名為 Mission_01.level .現在你的 .level 檔案被存到了 data\LevelData\Campaign\Postmortem\Mission_01 資料夾裡面.

4.2 建立 .lua 檔案
現在有了你的基本任務關卡檔案,你還需要建立你的任務 script . lua檔案是主要的任務script,他定義了你的任務中所有會用到的函式和事件.

給任務用的 .lua 檔案裡面包含了所有任務中會用到的規則.他的檔名必須和 .level 檔的檔名一樣.以這裡來說,他會是 Mission_01.lua .他們必須放在同一個目錄裡面.

4.2.1 範例 Mission.lua Script
下面是一個基礎的 mission.lua 檔:

-- 匯入 library 檔案,其中包含了所有的 helper 函式
dofilepath("data:scripts/SCAR/SCAR_Util.lua")

-- 變數可以在最外面宣告,讓他成為全域性(global)變數
obj_prim_newobj_id = 0

-- 呼叫OnInit,不然什麼都不會跑
function OnInit()
  -- 加入Rule_Init
  Rule_Add("Rule_Init")
  -- 因為 OnInit 不是規則,所以不用把他移除
end

-- 為了將任務標準化,我們都使用 Rule_Init 來讓事情開始動作
function Rule_Init()
  -- 這裡我們會呼叫這個任務最初的 Intelevent
  Event_Start( "intelevent_intro" )

  -- 加入用來決定任務結果的規則
   Rule_Add( "Rule_PlayerLoses" )

  Rule_Add( "Rule_PlayerWins" )

  -- 記得要移除 Rule_Init
  Rule_Remove( "Rule_Init" )
End

-- 這條規則會檢查玩家的母艦是否被摧毀
function Rule_PlayerLoses()
  if ( SobGroup_Empty( "Player_Mothership" ) == 1 ) then

    -- 目標失敗
    Objective_SetState( obj_prim_newobj_id, OS_Failed )

    -- 玩家輸掉這個任務
    setMissionComplete( 0 )

    Rule_Remove("Rule_PlayerLoses")
  end
end

-- 這條規則會檢查玩家是否摧毀了敵方的航空母艦
function Rule_PlayerWins()
  if ( SobGroup_Empty( "AI1_Carrier" ) == 1 ) then

    -- 目標達成
    Objective_SetState( obj_prim_newobj_id, OS_Complete )

    -- 玩家贏得任務
    setMissionComplete( 1 )

    Rule_Remove( "Rule_PlayerWins" )
  end
end

-- 別忘了建立事件表!
Events = {}

-- 這是開頭的 intelevent
Events.intelevent_intro =
{
  {
    { "Sound_EnableAllSpeech( 1 )",""},
    { "Sound_EnterIntelEvent()",""},
    { "Universe_EnableSkip(1)",""},
    HW2_LocationCardEvent( "Postmortem Tutorial", 5 ),
  },
  {
    HW2_Letterbox( 1 ),
    HW2_Wait( 2 ),
  },
  {
    HW2_SubTitleEvent( Actor_FleetCommand, "This is a tutorial script", 5 ),
  },
  {
    HW2_Wait( 1 ),
  },
  {
    { "obj_prim_newobj_id = Objective_Add("A new Objective", OT_Primary )", "" },
    { "Objective_AddDescription( obj_prim_startresourcing_id,'Description')", "" },
    HW2_SubTitleEvent( Actor_FleetIntel, "I've just issued a new objective", 4 ),
  },
  {
    HW2_Wait(1),
  },
  {
    HW2_Letterbox( 0 ),
    HW2_Wait(2),
    { "Universe_EnableSkip(0)", "" },
    { "Sound_ExitIntelEvent()","" },
  },
}

5.設定 .campaign 檔案

你需要為你的新戰役建立一個戰役檔案.戰役檔案指出了戰役的關卡存放在哪裡,他們的順序,動畫的一些資訊,諸如此類的東西.

為你的"分析用"戰役建立一個 Postmortem.campaign 檔案,放在 data\LevelData\Campaign 資料夾裡.這個檔案的內容應該如下:

-- =============================================================================
-- 名稱 : Postmortem.campaign
-- 目的 : 教學分析用戰役
--
-- Copyright Relic Entertainment, Inc. All rights reserved.
-- =============================================================================


-- DAT strings found in UI.DAT

-- 會在介面上顯示的名稱

displayName = "Postmortem"

-- 初始化
Mission = { }  -- 建立任務結構

-- Mission 1
Mission[1] = {

  postload = function () playAnimatic("data:animatics/A00.lua",1,1); end,
      -- 接著的這個函式告訴遊戲在任務載入之前撥放動畫 A00.lua
  directory = "Mission_01",
      -- 這行是說要從哪個資料夾中載入任務.以這裡來說是 Data/Leveldata/Campiagn/Postmortem/Mission_01
  level = "Mission_01.level",
      -- 要載入的任務lua檔
  postlevel = function ( bWin ) if ( bWin == 1 ) then playAnimatic("data:animatics/A01.lua", 1, 0) else postLevelComplete() end end,
      -- 告訴遊戲當任務結束時要做什麼.
  displayName = "Mission 1",
      -- 在戰役描述選單中會用到
  description = "Mission 1",
      -- 在戰役描述選單中會用到
}

當你想要增加新的任務的時候,你需要在檔案中增加新的任務項目.

6.雜項檔案

你可能能會想做一些額外的動作,像是方言化( Localization ),增加特定的AI script ,建立任務載入畫面,或者為任務中的每一個玩家設定隊伍顏色.這些檔案放在任務資料夾裡面,包括了 .dat 檔, Teamcolour.lua, AI.lua, ReferenceFleet.lua, datfiles.lua,以及 Mission.tga.

6.1 DAT 檔案
方言化的文字使用 ID 編號過的字串,當看到方言呼叫(localization call),像是 $42999 這一類的東西時,遊戲就會去查找定義在 .dat 檔案裡面的文字.例如:

42999 This is the localized text that would appear in the game

而這個是會去查找這行文字的的事件呼叫:

{
HW2_SubTitleEvent( Actor_FleetCommand, "$42999", 5 ),
},

以這裡來說,這行Script會顯示 Actor Fleet Command 並且顯示這行字幕:"This is the localized text that would appear in the game". 5標示出了文字會在螢幕上停留多久.以這裡來說是五秒鐘.

如果你有要用到音效檔案,遊戲會到 Data\Sound\english\Speech\MISSIONS 資料夾中尋找檔名和文字號碼相同的音效檔案.以這裡來說,音效檔案的名稱會是42999.fda

6.1.1 將你的任務方言化(Localize)
要將你的任務方言化,你需要在 data\Locale\English\LevelData\Campaign\ 裡面加入一個與之類似的戰役結構.在裡面你需要放置一個格式正確的 .dat 檔案,裡面包含了對應你的任務內容的文字.

接下來,你需要建立一個 datafiles.lua 檔案,和你的 mission.level 放在同一個資料夾裡面.這個檔案看起來會像這樣:

Dictionaries = { { name = "locale:leveldata/campaign/postmortem/mission_01.dat", }, }

6.2 Teamcolour.lua
這個檔案指定了任務中所有玩家艦隊的底色,條紋顏色,也可以指定艦徽

要自定任務中玩家的隊伍顏色,建立 teamcolor.lua 檔案並且把他和你的任務放在同一個資料夾中. teamcolor.lua 的範例如下:

-- [玩家索引號碼] = {{底色 R, G, B}, {條紋顏色 R, G, B}, "艦徽檔案.tga", {尾流顏色 R, G, B}, "尾流檔案.tga"},

teamcolours =
{
[0] =
{{.365,.553,.667},{.800,.800,.800},"DATA:Badges/Hiigaran.tga" ,{.365,.553,.667},"data:/effect/trails/hgn_trail_clr.tga"},
-- player

[1]=
{{.900,.900,.900},{.100,.100,.100},"DATA:Badges/Vaygr.tga" ,{.921,.75,.419},"data:/effect/trails/vgr_trail_clr.tga"}
-- vaygr
}

6.3 AI.lua
AI script. 他會覆蓋或是啟動AI函式.要增加某個特定的 AI 到你的關卡中,你必須增加一個 AIX.lua到你的關卡資料夾, X 是你想要自訂的 AI 的 ID 編號.

AIX.lua的內容已經超過了這份文件所預期要涵蓋的部分.

6.4 ReferenceFleet.lua
用來指定AI的難度.關卡裡面需要增加相對應的艦隊欄位.

6.5 Datfiles.lua
用來指定關卡會用到的 dat 檔案

6.6 Mission.tga
是的,這是載入關卡時會顯示的載入畫面.載入畫面是一個 .tga 檔,跟 .level 放在同一個目錄裡面,而且檔名也相同

7.附錄
•想要取得更多有關於 LUA 的資訊,請參考 http://www.lua.org 的線上文件
•推薦的 Lua 編輯程式? 你可以到 http://www.scintilla.org/SciTE.html 去取得 Scite
8.聲明
本文件主要是翻譯自Relic Developers Network所釋出的"HW2_SCAR.pdf".
翻譯者為西加拉宇宙載具改裝中心的CQD.原始文件版權為Relic所有.