主題

【教學】【RMMZ】JS 初學者要如何開始寫插件?(上篇)

十月 | 2021-11-19 21:41:47 | 巴幣 14 | 人氣 102

(文章略長,請先有心理準備)

# 前言

當 RPG Maker 改以 JavaScript 為遊戲語言之後,自認對 JS 有些許了解,我每隔一段時間便會興起加入 RM 圈,製作遊戲、撰寫插件的想法。

想來要做點小插件應該不是難事吧。

但現實殘酷,我總是沒辦法堅持下去(正因這一點,我相當偑服巴哈、Discord 上面各個正爆肝製作中或甚至已經完成過遊戲的人)。

JavaScript 大概是世界上最易於學習的語言之一了,然而要讀官方的程式邏輯與 JS 能力其實關聯不大——裡面用的寫法都很簡單,只是粗糙且不必要地複雜。只要有基礎,理應慢慢能懂。

那為什麼我還是會失敗放棄呢?要怎麼樣才能用簡單的程式,做出插件,修改預設功能?

我打算以我從全然不懂的 RM 新手,直到成功開始製作插件的這段經驗,寫點介紹與方法。需要注意的是,我在這以前已經學過了 JS ,我比較沒有「看不懂一段程式在做什麼」的困難。

如果你為了 RPG Maker 而開始學習 JavaScript ,正想知道如何在這裡中活用你的編程技能,那也許我可以給你一個參考起點。

如果你沒有 JavaScript 或其他語言的編程知識,那接下來可能會有相當多領域術語,如果遇到困難,無論如何都難以理解,那我建議可以先從語言本身開始學起(可以參考 [MDN] 網站),這篇文章並不是語言教學。

如果你已經對編程語言有相當了解,但對 RPG Maker 的程式碼並不清楚,那從第二章開始應該可以作為參考。

如果你已經是插件界的老手……這篇文章是寫給新手看的喔,對你來說可能會有很多無趣、乏味的內容。

任何人發現了文章中的錯誤或想提供補充參考,小至文字誤鍵,大至觀點偏誤,我隨時歡迎指教。

# 1. 為什麼會失敗

當我反思自己在 RM 上屢屢放棄的主因,大概便是那長到嚇人的程式碼。一上來就是強大的心理壓力,實在吃不消。

讓我們計算一下每個檔案的行數,這些數字經過四捨五入,因為官方目前仍持續更新版本,它們隨時可能變化。

```txt
main           150
plugins          x
rmmz_core     6400
rmmz_manager  3100
rmmz_object  11300
rmmz_scenes   3600
rmmz_sprites  3700
rmmz_windows  6600
```

加總後近三萬五千行。就算我讀了,也記不住,怎麼辦呢?

如果你想透過官方文檔來學習程式碼,那可能要失望了。文檔中僅包含了 `rmmz_core.js` 6400 行程式,其餘部份一字未提,但那才是形塑 RM 系統的主要部份啊。

這就好像電腦的維修手冊上,居然是高中電學一樣。不,我想知道它的主控板、記憶體,乃至操作系統的規格。

而解方很簡單,就是別讀。

研究目錄頁,對這本書的基本架構做概括性的理解。這就像是在出發旅行之前,要先看一下地圖一樣。
——《如何閱讀一本書》P.42

在面對他人所寫的程式碼時,也同樣可以從目錄來大略了解它寫了什麼,不需要立刻就讀完上萬行程式碼,只要等到需要的時候,再以查詢百科全書的心態來探索就好。

# 2. 了解如何取值

當然,我們終究要讀一些程式。既然沒有看,知道自己要查什麼東西就變得很重要。

### 變數、開關與角色

`rmmz_object.js` 裡,可以看到像是:

- `Game_Variables`:控制遊戲變數。
- `Game_Switches`:控制遊戲開關。
- `Game_Actor`:控制遊戲角色。

所有這樣的單例,在執行期會被放入以 `$game` 開頭的變數中。你可以開啟一個專案,在測試運行的控制臺中察看。

至於我們在編輯器資料庫介面規劃的道具、角色等等設定,則是存放在以 `$data` 開頭的參數。

因此,我們能用如下的片段,在 RM 中玩 FizzBuzz :

```js
(() => {
    // 取得1號變數。
    const value = $gameVariables.value(1)
    
    // 依據規則,返回數字對應的字串。
    function getValueSign(rules, value) {
        const res = Object
            .keys(rules)
            .reduce((res, key) => value % Number(key) === 0
                ? res + rules[key]
                : res
            , "")
        
        return res.length === 0 ? value.toString() : res
    }
    
    // 以上限值取得一段範圍的 FizzBuzz 字串。
    function getFizzBuzzByRange(limit) {
        const rules = {
            3: "Fizz",
            5: "Buzz",
        }    
        const getFizzBuzz = value => getValueSign(rules, value)
        
        return [...Array(limit)]
            .map((_, idx) => getFizzBuzz(idx + 1))
            .join("\n")
    }
    
    // 依據1號變數產生對應 FizzBuzz 字串。
    const result = getFizzBuzzByRange(value)
    
    // 設定2號變數。
    $gameVariables.setValue(2, result)
})()
```

### 如何找到自己需要的值

`rmmz_object.js` 中,我們可以找到大部份所需的東西,甚至是事件頁功能的對應函數也有。

`rmmz_scenes.js``rmmz_sprites.js``rmmz_windows.js` 三個檔案則是負責處理畫面、介面等等功能。這部份比較複雜些,不在本文的介紹範圍內。

你可以透過瀏覽大綱,以了解那些功能或事物在 RM 程式碼中的具體用詞,以便在遇到相關問題時知道以什麼關鍵字搜尋。

值得一提的是,那些事件功能對應的函數有部份可能並非那麼開箱即用,因為它們是以「在事件物件環境下執行」為前提而撰寫。

例如:若我想在執行 FizzBuzz 函數時立刻看到結果,又懶得再加顯示訊息的事件指令。但是直接使用顯示訊息的 `command101` 函數卻會報錯,因為它想提前看到下一條事件指令,以準備像是選擇項之類的情況。

於是我需要檢查訊息系統,並寫成這樣:

```js
(() => {
    // ...
    
    // 設定為滾動字幕
    $gameMessage.setScroll(5, false)
    // 加入至待處理的文字序列中。
    $gameMessage.add(result)
})()
```

# 範例1:清除畫面上的所有圖片

### 目標與猜想

假設我們製作的遊戲使用了大量圖片來組合成獨特的介面,這些圖片還可能因為條件不同,於是編號也不同。那當要關閉介面的時候怎麼辦?

僅管當然可以老老實實寫好幾條指令把圖片遂一關掉,但這樣既浪費效能、容易遺漏,如果要改介面,也不方便。

由於圖片的運作方式我們可以猜測,它可能是某種陣列。因為在編輯器中,我們只能透過「編號」來指定圖片,它沒有名稱標籤,而對於這類數據最簡單的方法就是陣列——它不會無緣無故用物件來裝以編號為鍵的東西,在範例2中我們可以看到另一個例子。

### 查找自己需要的程式段

既然是陣列,那一定有一個地方寫著 `變數名稱 = []` 。而程式中提到圖片的有兩個地方:

- `rmmz_manager.js``ImageManager`
- `rmmz_object.js``Game_Picture`

但整段瀏覽下來,`ImageManager` 其實是用來載入圖片檔案;`Game_Picture` 則是包裝圖片應該如何顯示的數據。

那就搜尋 `picture` 這個關鍵字吧。看樣子應該有個變數,會被用來裝一大堆 `Game_Picture` 才對。但圖片屬於遊戲執行過程中才逐一加入的物件,可能沒辦法用 `Game_Picture` 直接找到目標。

等等,`picture``rmmz_object.js` 居然使用了超過一百次?

我們可以發現除了 `Game_Picture` 之外,用最兇的是 `Game_Screen` ,而正好就在第一個搜索結果,我們有 `this.clearPictures();`

```js
Game_Screen.prototype.clearPictures = function () {
    this._pictures = [];
};
```

### 確認效果與保險起見

就是這個了,我們只需要調用 `$gameScreen.clearPictures()` ,遊戲在下一幀就會把圖片清除掉。

保險起見,讓我們再深入一點,萬一它在刪去圖片以前,還需要做什麼事情呢。

就在這段下方,寫著 `Game_Screen.prototype.eraseBattlePictures` ,這是用來清除戰鬥時的圖片,它十分簡單地用了 `Array.slice`

看來我們不用擔心太多,直接跳到第二個帶有 `erase``Game_Screen.prototype.erasePicture` ,這裡更是直接把對應元素設置為 `null` 而已。

所以沒問題,我們可以直接使用 `$gameScreen.clearPictures()` 來刪除所有圖片。安全、乾淨、無汙染。

# 小結:視為香料調味

就像上面的範例都可以使用事件的腳本選項執行,在許多功能中,編輯器都留下了可以寫「腳本(或者叫做 Snippet)」的空間,也許是在技能傷害公式、道具效果執行公共事件(又譯做一般事件)、條件判斷使用腳本等等。

如果編輯器裡面沒有我們需要的資料欄位——也許我們想讓主角的名字,依據武器裝備而不同——那也可以到武器的「注釋」欄位中定義,如 `<name:小明>` (如果冒號後面有空格,也會一併包含進去),並用 `$dataWeapons[武器編號].meta` 得到 `{name: "小明"}`

只要能夠活用這些功能,就能簡化許多原本需要複雜過程的做法,提供更多可能。

創作回應

相關創作

更多創作