蒼時弦也
蒼時弦也
資深軟體工程師
發表於

自從畢製開始與同學開發遊戲後,我就開始喜歡嘗試運用一些工具如 HTML5、Mono、Processing 等來製作一些屬於自己的「遊戲框架」

自從上次嘗試使用 Mono 與 mruby 結合後,這次在與朋友的閒聊中回想起了 Open Frameworks 這套工具。 Open Frameworks 基本上被稱為是 C++ 版本的 Processing 就各方面來說比 Processing 改進不少,至少就我這幾天的體驗來看,以我目前的實力已經可以純熟運用了!

過去曾有一段時間嘗試玩過,但是因為沒有 Project Generator 輔助建構專案,再加上與 C++ 其實不是那麼的熟悉,因而放棄。這次透過 Unreal Engine 的經驗,以及上次 mruby 的整合讓我順利的開始使用 Open Frameworks。

這篇文章主要會分享我使用 Open Frameworks 開啟一個 Ruby 檔案,並且執行裡面的方法在介面中繪製圖像的做法。 目前我認為這個方法其實還不太完善,不過作為初次的嘗試可以算是一個不錯的成果。

首先,要使用 mruby 必須先有 mruby 才行,關於這部分請直接參考「mruby in C# 因 RPG Maker的慘劇(一)」這篇文章,裡面會詳細說明建構 Static Library 的方法。

Open Frameworks 目前建置出來的是 32bit 的版本,因此跟 Mono 的情境一樣需要開啟 32bit 的編譯選項

配置 XCode 專案

在 Open Frameworks 0.8 之後已經支援 Retina 顯示,關於這部分可以直接 Google 相關資料就不多做解釋了(作法也很簡單,在 .plist 加入選項即可,雖然整體使用上還不夠理想⋯⋯)

為了要使用 mruby 的套件,我們需要在專案面板中手動加入函式庫。

螢幕快照 2015-02-18 下午7.26.02.png

做法不難,在 Linked Frameworks and Libraries 新增剛剛編譯好的 libmruby.alibmruby_core.a 即可。

libmruby_core.a 是選用的,裡面實作了一些 Ruby 基本的功能建議加入(不然只會拿到幾乎是什麼都沒有的 Ruby 環境)

另一方面我們需要增加 Header 的設置。

螢幕快照 2015-02-18 下午7.29.11.png

在 Build Settings 的 Tab 裡面找到「Header Search Path」並且加入即可。

也許會找不到,可以把左上角的「Basic」換成「All」就能看到了!

裡面的路徑我寫了 $(HEADER_MRUBY) 是因為 Open Frameworks 有一個 xcconfig 的設定檔,基於實驗精神我做了測試,這邊可以直接寫上路徑(相對、絕對路徑都可以)

這樣基本上就配置好了開發環境,不過我想是有更乾淨的配置方式。 不過基於我使用 XCode 也不過幾個月,這也是第一次用 XCode 引用外部的函式庫,就先這樣解決吧!

mruby 運行環境

在 mruby 的 API 中我們可以透過 mrb_open() 以及 mrb_close() 來開啟跟關閉一個 mrb_state (也許稱作 context 會更好)總之,我們可以產生多個運行的環境,為了方便起見包裝成一個 Class 來呼叫。

 1
 2#include <string.h>
 3#include <mruby.h>
 4#include <mruby/compile.h>
 5
 6// 使用 string.h 是因為 Open Framrworks 大部份都是傳回 string 而非 const char *
 7// 我們會需要使用 mruby/compile.h 裡面含有從檔案讀取等處理,若要直接執行 .rb 檔案則需要引用
 8
 9using namespace std;
10
11class Ruby {
12public:
13  Ruby();
14  void load_file(string fileName);
15  void call(string methodName);
16  void close();
17private:
18  mrb_state* mrb;
19}
 1
 2#include "Ruby.h"
 3
 4Ruby::Ruby() {
 5  mrb = mrb_open();
 6  
 7  // 這邊之後會加入 ofImage 的 Binding 程式
 8}
 9
10void Ruby::close() {
11  mrb_close(mrb);
12}
13
14void Ruby::load_file(string fileName) {
15  FILE* file = fopen(fileName.c_str(), "r");
16  mrb_load_file(mrb, file); // 實際上回傳回 mrb_value 不過我們不需要
17  /*
18  if(mrb->exc) {
19   // 如果發生錯誤(Error)可以在這邊做對應處理,因為這個範例功能簡單所以就不多做討論
20  }
21  */
22  fclose(mrb);
23}
24
25void Ruby::call(string methodName) {
26  /**
27    mrb_funcall() 的 API 如下
28    mrb_state* -> 運行的 Ruby Context
29    RClass* -> 呼叫方法的物件,使用 mrb_top_self(mrb) 可以直接呼叫非物件的方法(這與 Ruby 語言設計有關)
30    const char * -> 呼叫的方法
31    int -> 方法的參數
32    * -> 一次傳入各種 Ruby 參數(由前面的參數決定傳入數)
33  */
34  mrb_funcall(mrb, mrb_top_self(mrb), methodName.c_str(), 0); // 因為只要單純的呼叫,所以不多處理
35}

如此一來,我們就可以利用類似下面的程式碼來執行某個 Ruby 檔案:

Ruby* Ruby = new Ruby;
Ruby->load_file("app.rb");
Ruby->call("hello_world");
Ruby->close();

ofImage 的 Binding

我的目標只有兩個,所以後續的實作也會基於這兩個實作:

  1. 讀取圖片
  2. 繪製在畫面上的某個位置
 1
 2#include <ofMain.h>
 3#include <mruby.h>
 4#include <mruby/string.h>
 5#include <mruby/data.h>
 6#include <mruby/class.h>
 7
 8using namespace std;
 9
10namespace Ruby {
11  class Image {
12  public:
13    static void setup(mrb_state* mrb);
14  protected:
15    // mruby 的 Method 都是傳回 mrb_value 並且接收 mrb_state 與 mrb_value (物件本身) 作為參數
16    // 這邊實作 initialize() 方法是因為我們的物件需要儲存 ofImage 的參照讓我們可以在同一個物件實例中對其操作
17    static struct mrb_value initialize(mrb_state* mrb, mrb_value self);
18    static struct mrb_value loadImage(mrb_state* mrb, mrb_value self);
19    static struct mrb_value drawImage(mrb_state* mrb, mrb_value self);
20  }
21}

這個檔案會是目前最多程式碼的部分,裡面有一些其實應該移出來放到新的檔案。 不過為了撰寫方便,所以寫在這個檔案中。

 1
 2#include "Image.h"
 3
 4using namespace Ruby;
 5
 6// 定義 Image Class 的資料結構
 7struct mrb_of_image {
 8  ofImage* instance;
 9}
10
11// 定義釋放記憶體的方法
12// 因為後面會使用 malloc 產生 mrb_of_image 這筆資料,而 Ruby 本身也有 GC (垃圾回收)的機制
13// 因此推測是用於 GC 時能夠順利清除這筆記憶體
14static void mrb_of_image_free(mrb_state* mrb, void *ptr) {
15  mrb_free(mrb, ptr);
16}
17
18// 定義配置記憶體的方法
19// 因為如果直接在某個方法中儲存 ofImage 參照會被清除,因此使用 malloc 保持(而回收則交給 Ruby 的 GC 機制)
20static struct mrb_of_image* mrb_of_image_alloc(mrb_state* mrb) {
21  mrb_of_image* image;
22  image = (struct mrb_of_image*) mrb_malloc(mrb, sizeof(struct mrb_of_image));
23  // 這邊可以視情況做各種初始化
24  image->instance = new ofImage; // 這裏預先初始化了 ofImage 物件
25  return image;
26}
27
28// 定義 mruby 中的資料類型(Data Type)
29static struct mrb_data_type mrb_of_image_type = { "Image", mrb_of_image_free }
30
31Image::setup(mrb_state* mrb) {
32  // mrb_define_class 回傳回一個 RClass 參照,而第三個參數是「繼承」自哪個物件,這邊從 Ruby 的 Object 繼承(Ruby 預設)
33  struct RClass* klass = mrb_define_class(mrb, "Image", mrb->object_class);
34  
35  // 定義 Image Class 的方法
36  //
37  // API 中會有 mrb_define_class_method() 和 mrb_define_method() 兩個方法,而且會讓人覺得疑惑
38  // 實際上,使用 define_class_method 的時候,產生的是「靜態方法」 Ex. Image.loadImage()
39  // 而使用 define_method() 則是「實例的方法」 Ex. image.loadImage() // image = Image.new
40  
41  // mruby 中有預先定義好的巨集 ARGS_* 可以輔助我們指定傳入參數的條件
42  mrb_define_method(mrb, klass, "initialize", Image::initialize, ARGS_NONE());
43  mrb_define_method(mrb, klass, "load_image", Image::loadImage, ARGS_REQ(1));
44  mrb_define_method(mrb, klass, "draw", Image::drawImage, ARGS_REQ(2));
45}
46
47/**
48 * 實作 Image Class 方法
49 */
50 
51mrb_value Image::initialize(mrb_state* mrb, mrb_value self) {
52  struct mrb_of_image *image;
53  image = (struct mrb_of_image*) DATA_PTR(self); // DATA_PTR 可以取出儲存於物件中的 Data 資訊
54  if(image) {
55    mrb_of_image_free(mrb, image); // 清除(這個記憶體位置中的資料不會被使用,因此需要被釋放掉)
56  }
57  
58  DATA_TYPE(self) = &mrb_of_image_type; // 確保物件的 Data Type 被辨識為自定義的 mrb_of_image_type
59  DATA_PTR(self) = NULL; // 清空物件中的 Data
60  
61  image = mrb_of_image_alloc(mrb); // 重新初始化 
62  
63  DATA_PTR(self) = image; // 將正確的 Data 設定上去
64
65  return self; // 沒有特殊需求就傳回自己,也讓 Ruby 的呼叫擁有可以 Chian 的性質
66}
67
68mrb_value Image::initialize(mrb_state* mrb, mrb_value self) {
69
70  mrb_value mrbFilePath; // 儲存於 Ruby 中的路徑資訊
71  mrb_get_args(mrb, "S", &mrbFilePath); // 將 Method 傳述的參數解析出來(在 mruby 是利用這種方法讀取的)
72  const char * filePath = mrb_string_value_ptr(mrb, mrbFilePath); // 將 mrb_value 轉為 char 陣列
73  
74  struct mrb_of_image* image = (struct mrb_of_image*) DATA_PRT(self); // 取出物件中的 Data 資訊
75  // 呼叫 ofImage 的 loadImage 進行讀取圖片
76  // ofToDataPath() 可以將路徑轉為正確的 data/ 目錄路徑(像是 OSX 的 App 會被包在裡面,預設會讀錯位置)
77  // 因為接受的是 string 參數,因此直接將 char 陣列轉為 string
78  image->instance->loadImage(ofToDataPath(string(filePath)));
79
80  return self;
81}
82
83mrb_value Image::initialize(mrb_state* mrb, mrb_value self) {
84
85  mrb_float x, y; // mrb_float 可以看作 float 的別名,可以直接當作 float 使用( mruby 會看情況選用 float / double )
86  mrb_get_args(mrb, "ff", &x, &y); // 取出參數(這次是 float 類型)
87  
88  struct mrb_of_image* image = (struct mrb_of_image*) DATA_PRT(self);
89  image->instance->draw(x, y); 
90  
91  return self;
92}

到此為止,我們就算是完成 ofImage 的 loadImage / draw 的 Binding 了!

從 Open Frameworks 運行 Ruby

接下來在 ofApp.cpp 中做一些處置就可以執行我們要的 Ruby 檔案了!

 1// 略
 2// Header 中應該要已經寫好 Ruby* Ruby; 的設定
 3void ofApp::setup() {
 4  Ruby = new Ruby;
 5  Ruby->load_file(ofToDataPath("load_image.rb"));
 6  Ruby->call("setup");
 7}
 8
 9void ofApp::update() {
10  Ruby->call("update");
11}
12
13void ofApp::draw() {
14  Ruby->call("draw");
15}
16
17void ofApp::exit() {
18  Ruby->close();
19}
20
21
22// 略

這樣我們就會去讀取 data 目錄下的 load_image.rb 這個檔案。

接著在 data 目錄新增 load_image.rb 然後運行看看吧!

 1
 2$image = Image.new
 3$imageX = 0
 4$counter = 0
 5
 6def setup
 7  $image.load_image "images/logo.png"
 8end
 9
10def update 
11  $counter += 1
12  $imageX = $counter % 500
13  $counter = 0 if $imageX === 0
14end
15
16def draw
17  $image.draw $imageX, $imageX
18end

雖然 Ruby 的部分會用到全域變數之類的看起來不太習慣,不過至少可以讓 C++ 跟 mruby 互相溝通了!

這次學到不少新的用法,我想很快就會忘記所以就趕緊寫篇筆記記錄下來。

參考資料: