談談AVG遊戲的Android移植(NScripter與吉里吉里)

大家好,很久不見,小弟最近閉關修煉iPhone中,所以很長時間沒更新博文(順便在寫某物的C++版,另外某物0.3.2版與WP7版已構建完成,不久就會發布)。這次回來,先換個與某物無關的話題,以目前用戶量最大的NScripter(簡稱NS,以下同)與Krkr2(吉里吉里2)爲代表,來簡單談談 AVG遊戲的Android環境移植吧。

______________


關於NScripter的Android版移植:


ONS和SDL:


大家都知道,日本人高橋直樹是NScripter項目的發起者。

然而,事實上高橋直樹開發的原始版NS程式早在09年就已經停止了更新,現今已很難再看見利用原版NS開發的程式。那麼,NS爲何還能有現在這麼龐大的用戶支持率呢?答案很簡單,一切都要歸功於ONS的存在。應當說,目前應用最廣,也是真正讓NS腳本發揚光大的,還要數第三方製作的ONScripter,這一完整支持NS腳本的跨平臺AVG引擎不可(簡稱ONS,以下同)。


單獨從編程角度上講,ONS不等同於NS,由於高橋氏開發的NS程式並沒有開放源碼,因此ONS是通過黑盒方式參考NS效果自行模擬出的ONS功能(這個過程有點像製作遊戲機模擬器),所以它並不是一個代碼移植品,而應視同一個獨立於NS原版的新型NS腳本解釋與執行器。除了能解讀同樣的遊戲腳本外,它與 NS就沒有任何程式上的繼承關係。

ONS相比較NS的最大優勢在於,ONS和完全依賴DirectX渲染僅支持Windows系統的NS不同,它採用了一代神人Slouken製作的SDL框架進行腳本與計算機設備交互,天生具備SDL框架“駭人聽聞”的跨平臺移植能力。


無論是windows、linux、mac抑或PSP、PS3、PS2gs乃至wince、iphone、ipod、android平臺都能看到它活躍的身影(當然,這需要相關的本地運行庫配合,比如從渲染角度上講,在Windows繪製畫面既可以使用SDL提供的DirectX封裝又可以使用OpenGL 封裝,而到了Linux環境就只能使用OpenGL庫,到了智能機或者掌機環境就要轉到OpenGLES庫,這些都必須有人提供相關的本地化封裝,然後才能通過統一的API進行調用。即便所寫代碼在API層面高度一致,但在不同環境中的具體實現依舊是有差異的。也就是說,如果某個平臺並沒有必要的運行庫支持,那麼SDL也無法在該平臺編譯與運行。而SDL的強悍就體現在,它所提供的本地運行庫相當完整,幾乎涵蓋了所有主流系統,兼容性卻又相當優異)。

憑藉這種優勢,目前大家所見的,絕大多數使用NS腳本開發的AVG遊戲(或者狹義的指galgame),大多是以ONS而非原版NS作爲運行環境——在掌機和智能機上尤其如此。

可以說,沒有SDL的成功,就沒有今天的NS(ONS)的輝煌。

這裏吐個槽,前一陣小弟在某書店讀到某Android教程,其中以NDK5編譯了某版本的《雷神之錘》,而後就反覆強調NDK移植C/C++遊戲是多麼方便,多麼簡單之類的。小弟以爲,這種說法實在有些忽悠了。衆所周知,網上能找到的開源版《雷神之錘》(http://www.libsdl.org/projects/quake/),使用的就是SDL這個目前世界上兼容性最強的跨平臺引擎(而SDL子項目http://libsdl-android.sourceforge.net/,早已提供了SDL與Android設備的完整交互支持)。因此,即便NDK5能正常編譯SDL開發的遊戲,也只能證明NDK5的基本功能正常(Android好歹也是Linux核心,如果SDL在上面都跑不起來,讓Google情何以堪啊),卻無法理解成NDK開發有多麼便捷,更不能代表所有 C/C++遊戲都能輕鬆移植到Android環境當中(此前小弟曾和某友談及怎麼常有人能把DOS遊戲移植到Android環境中運行的問題,小弟在此粗談兩點:一、世上有個開源項目叫DosBox,能夠跨平臺模擬標準DOS運行環境。二、DosBox是以SDL爲核心開發的),就別提完全取代Java開發模式了。

我們登錄http://onscripter.sourceforge.jp,就可以獲得關於標準版ONS的詳細介紹與各版本下載路徑。

至於ONS-Android,則是由ONS作者提供的,完全實現了ONS功能的,專門用於跑在Android平臺的ONS版本。如果您要在Android上進行NS遊戲移植,首先就離不開ONS-Android的下載與編譯。


ONS-Android的編譯:


ONS-Android的編譯,僅需要如下步驟就可以做到。

1 下載SDL的Android版擴展庫


由於標準版SDL源碼包中尚未包括Android本地化支持,所以我們需要單獨下載Android版SDL源碼包,才能在Android環境中正常編譯與運行SDL程序。事實上,所有想使用SDL以C/C++方式開發Android遊戲的用戶,也會需要這個SDL運行庫的支持。

下載地址:http://libsdl-android.sourceforge.net/

(PS: ONScripter-Android內置已是這個運行庫,不必真正下載,此處僅說明來源)。

2 下載Android版ONScripter


下載地址:http://onscripter.sourceforge.jp/android/onscripter_android.tar.gz

由於ONS-Android採用JNI方式,進行C/C++部分與Java部分的交互,因此下載後的源代碼也就同時包含了java(功能集中在遊戲載入,界面初始化與JNI調用)與純C(sdl-android支持庫)兩大部分的源碼。

不過,真正解釋執行NS腳本的ONScripter本體(C++實現)這時卻並沒有包含在內(估計是作者考慮到ONScripter核心代碼是所有平臺共通的,纔沒有直接放入ONS-Android當中)。

現在,我們還需要下載ONScripter的核心源碼部分,才能真正進行ONS-Android編譯。

3 下載標準版ONScripter


下載地址(也可選其它版本):http://onscripter.sourceforge.jp/onscripter-20110619.tar.gz

好了,編譯ONS-Android的要素全部齊備了。

現在,將最後下載的ONS核心源碼解壓,並放入onscripter_android的jni\application文件夾下(建議解壓時不要改名,因爲 ONS的Android.mk配置裏默認就是編譯onscripter*下文件,改名很可能導致找不到目標文件(除非您重寫了Android.mk配置))。

這時,我們只要通過NDK編譯onscripter_android項目,就能立刻得到相關的so文件了(累計將編譯出九個文件,其中只有libapplication.so爲ONS運行庫,其餘爲SDL支持庫),相當之簡單吧?



下圖爲通過Cygwin在Windows中編譯ONS的畫面。
 

可以說,只要系統環境設定正常,原始ONS-Android配置已可保證編譯成功。假如在編譯時提示找不到某某文件,則說明環境變量中缺少必要的系統支持庫路徑,並非onscripter_android源碼不全,請自行在Android.mk添加相關路徑或者修改Cygwin環境配置,具體請參考NDK開發文檔或Cygwin使用文檔(當然,直接Ubuntu編譯最簡單)。

編譯成功後,獲得的so文件列表:

 

有了so支持庫與Java代碼,任何稍有Android(或Java)開發經驗者,都能輕易將NS遊戲運行於Android系統之上。

ONS-Android的漢化問題:

出於衆所周知的原因,ONS-Android默認並不支持中文編碼(它是日本人做的)。這樣的設計,在使用原版ONS-Android運行日文遊戲時並不會有太大問題(只要該遊戲沒有調用第三方插件,沒有使用額外API)。但是,一旦我們想要讓它跑一些經過漢化的中文編碼遊戲呢?顯而易見,肯定會造成亂碼的出現。


所以,如果我們想要ONS-Android能夠正常地進行中文遊戲顯示,在進行ONS-Android編譯時,便需修改其部分代碼。更準確地說——是修改位於ONS核心包下的sjis2utf16.cpp文件來解決這一問題。

總體來講,ONS的字符解碼過程並不複雜,sjis2utf16.cpp中僅有initSJIS2UTF16(初始化解碼錶到sjis_2_utf16這一數組中)、convSJIS2UTF16(轉化日文編碼SJIS爲UTF-16編碼)、convUTF16ToUTF8(轉化UTF-16爲UTF-8編碼)這三個函數在起作用。而ONS的所有字符解碼部分也會經由調用convSJIS2UTF16和convUTF16ToUTF8這兩個函數產生作用(注意,initSJIS2UTF16是sjis_2_utf16變量初始化賦值時使用的函數,僅會在ONS啓動時調用一次)。


知道了這些,解決漢化問題就變得非常簡單,只要根據現有的ONS解碼規則,將其sjis_2_utf16_org數組中的SJIS日文編碼表轉化爲我們需要的中文編碼表(GBK也好,Big5也好,原理都一樣,一個指定編碼對應一個相對的UTF-16編碼,然後以二維數組形式保存),就能夠非常輕鬆的實現 ONS中文解碼,甚至不需改寫任何邏輯代碼(如果有某些字符需要特殊過濾,也可以修改convSJIS2UTF16和convUTF16ToUTF8這兩個函數進行攔截)。編碼表較大,下文有相關下載地址。

當然,如果我們想保留原版sjis2utf16.cpp內容也沒問題,大可以新建一個gbk2utf16.cpp之類的文件,讓兩套解碼器並行存在,按需求進行切換(比如想根據手機環境自適配字符解碼器)。怎麼做呢?粗讀ONS源碼我們可以發現,它實際調用到字符解碼器的部分,僅集中於DirectReader.cpp和 ONScripterLabel.cpp、ONScripterLabel_text.cpp這三個源文件當中。具體的說,在於DirectReader 中的convertFromSJISToUTF8函數(其中同時調用瞭解碼器的convSJIS2UTF16與convUTF16ToUTF8函數。 PS:該文件中還有convertFromSJISToEUC函數,是給資源文件名解碼的,如果文件名中沒有稀奇古怪字符的話原則上可以忽視不管,如果有的話也需要進行適當修改),以及ONScripterLabel_text.cpp中的drawGlyph函數(調用convSJIS2UTF16)和 ONScripterLabel中的initSDL函數(調用initSJIS2UTF16)。如果想自適配解碼器,只需創建出相關的解碼用函數(放在 gbk2utf16.cpp、big52utf16.cpp隨便什麼中原理都一樣,改個編碼表而已),繼而通過最簡單的if……else……判定即可(如果想根據編譯環境判定,直接#if defined也行)。


總之,ONS中文解碼並不是什麼複雜的問題,漢字編碼表可以去http://unicode.org查詢,或者參考Emacs項目提供的map文件(下載一個Emacs,解壓後它的etc/charsets文件夾下全是後綴爲.map的編碼表)。還有,很久以前有人用google code發佈過ONS的gbk解碼版本(http://onscripter-cn.googlecode.com/svn/trunk/),有需要的朋友可以從svn下載過來參考。



ONS-Android的運行機制:


單從Java編程角度來說,ONS-Android的結構可謂非常簡單,開放給用戶的僅有Audio.java、DataDownloader.java、 GLSurfaceView_SDL.java、ONScripter.java、Video.java這五個Java文件而已(因爲具體實現是 C/C++的)。其中Video.java最爲重要,它包含有DemoRenderer和DemoGLSurfaceView兩個子類,這不單是ONS- Android的渲染核心,而且包含了nativeInit, nativeInitJavaCallbacks, nativeDone,nativeResize,nativeMouse,nativeKey等六個jni接口。其中最主要的接口是 nativeInitJavaCallbacks以及nativeInit,只有執行了這兩個jni函數,才能真正啓動ONS(本質上是先啓動SDL框架,附帶喚醒ONS)。


而執行nativeInit函數時需要注入的地址字符串,默認來源於 ONScripter類下的gCurrentDirectoryPath這個靜態變量。不管我們往gCurrentDirectoryPath中放入什麼字符串,默認情況下ONS都會按照這個字符串去讀取/保存遊戲資源(如果不能找到該路徑,則ONS會立即崩掉)。

至於其它,nativeDone是關閉SDL(由於ONS依賴於SDL,所以這些操作實際上都會先執行SDL部分,然後才轉到ONS部分,以下同),nativeResize是改變SDL畫面大小,nativeMouse觸發SDL屏幕操作,nativeKey觸發SDL鍵盤操作,皆屬基礎操作,並沒太多好講解的。

PS:上述部分jni源碼位於onscripter_android\jni\sdl\src\video\android文件夾下,如果要修改Java類名或接口名,請注意同時修改相關C代碼。

而 ONS-Android的啓動用Activity是ONScripter類,其中真正調用ONS運行的只有runSDLApp這個私有函數,至於 runLauncher及runDownloader函數一者爲要求遊戲用戶選擇ONS資源所在路徑,一者爲通過網絡下載ONS遊戲資源,都只會在 runSDLApp執行前調用,也不會實際觸發jni接口(只有runSDLApp觸發jni操作)。所以,如果您不想讓遊戲用戶使用您的APK啓動其它人提供的遊戲資源(也就是不想被人當模擬器用),則可以刪除runLauncher函數,或者固定gCurrentDirectoryPath變量中路徑字符串即可。

再者,遊戲初始化後默認顯示於畫面右側的僅僅是Java編碼的ONS操作按鈕(如ESC、Skip 這些),它們僅會通過JNI方式調用相應的SDL API,而不會真正影響C/C++實現部分。因此,如果您需要改變它們的顯示位置,或者進行刪除、修改這些按鈕的操作,乃至用其它方式徹底替代它們(比如將相關JNI調用放入Menu中,按下菜單鍵時才起作用),都只需修改相關Java代碼即可,無需改變任何C/C++部分。

另外,通過解讀源碼我們可以獲悉,在ONS-Android初始化運行時,有三種文件必不可少。

一是腳本文件,它可以命名爲0.txt、00.txt、nscr_sec.dat、nscript.___、nscript.dat這五種名稱中的任意一種(後三種都屬於NS的dat文件包,需要通過工具打包,網絡有下載),但除此之外的名稱一概不認,沒有則無法運行遊戲。

二是遊戲資源文件,也就是NS中使用的arc.nsa,遊戲中使用的音頻與圖像文件強制要求打成此包(NS提供有打包工具),並且保持此名稱不變,否則ONS還是不認。

三是default.ttf,也就是字庫文件,由於ONS-Android默認情況下不能使用Android字庫,所以此文件必須存在,並且正常可讀(必須能夠被SDL識別),否則遊戲會強制退出(沒錯,如果字庫不存在或者讀取異常,ONS直接就崩了~連錯誤都不報~)。有了上述三種文件,ONS才能在 Android中正常運行。


最後,雖然AVG遊戲通常需要較大的音頻與圖像文件支持,Android版ONS默認要求將遊戲資源文件保存於SD卡中。但經實測發現,如果將gCurrentDirectoryPath設定爲其它可讀寫目錄,只要上述三文件正常無誤,ONS-Android一樣能正常運行(比如APK的Cache文件夾)。所以,如果我們想對遊戲資源作一些手腳(在內存而非SD卡中進行遊戲什麼的),完全可以根據此特性入手。


ONS-Android的運行效果:

真機運行效果:



模擬器運行效果:

(雖然ONS-Android默認使用GLES,可惜Android模擬器默認只支持PixelFlinger,因此建議真機測試,否則速度相當悲劇)



關於Krkr2(吉里吉里2)的Android版移植


談完了ONS的移植,下面再來談談Krkr2這個著名的AVG引擎(其實,主要是做galgame~)的Android移植問題。

那麼,開章明義,大家請首先注意好這一句話:

在Android中“直接運行”Krkr2遊戲是不可能的


雖然僅就Windows系統而言,Krkr2遊戲量遠遠超過NS遊戲在這一領域的數量。並且Krkr2系統無論在操作方式、畫面效果、腳本功能都較古老的 NS系統更爲先進。然而,以下三點因素卻決定了在Android系統中,向ONS那樣直接運行Krkr2遊戲資源將非常難以實現(其實任何系統都一樣)。

一、首先,ONS源碼不算SDL支持庫,其大小僅有1MB左右,可謂短小精悍。而Krkr2呢? 僅core包下核心源碼就有將近10MB,代碼量比ONS多了將近10倍(還不算plugins包下的一些必要插件),僅此一項就讓移植Krkr2比移植 ONS所面對的工作量大了許多(代碼越多,出Bug的機會也就越高)。

二、其次,ONS直接使用SDL框架開發,令它先天具備相當強大的跨平臺特性(前文已述,SDL純C打造,正宗的“一次編寫,隨處編譯,隨處運行”)。反觀吉里吉里,W.Dee獨立開發的 Krkr2核心顯然沒能達到SDL的高度,他在開發之初甚至就沒考慮過跨平臺的問題,因此要將它跨平臺移植時所面臨的問題也就可想而知。

比如Krkr2默認使用DirectX而非OpenGL,又不像SDL那樣爲每個平臺都製作了必要的自適配圖形接口,導致它的渲染功能被綁死在 Windows平臺上,如果要移植只能重寫渲染部分不算;最要命的是,它還大量使用win32 API,先天與Windows系統難以分離,否則很多效果都要從頭摸索如何實現;並且,Krkr2還非常隨性的提供第三方接口,毫無原則的允許大量第三方插件跑在Krkr2之上,直接導致很多遊戲根本不是僅僅解決Krkr2運行環境就能夠移植的。事實上,如果想讓Krkr2正常運行在非Windows平臺,沒有相當大規模的代碼重構(注意,是重構,而不是簡簡單單的修改),根本不可能實現(總不能在Android上跑個Windows模擬器吧|||)。事實上,Krkr作者爲解決跨平臺問題,自2005年起已着手製作Krkr3,至今依舊努力中……


三、最後,Krkr2基本結構是由一套C++核心(含基於DirectX的圖像渲染部分及tjs腳本解釋器與kag腳本解釋器以及其它功能的複合),一套tjs 腳本庫(依賴C++編譯出的解釋器運行,Krkr2特有的腳本語言,語法類Java),以及一套構建於tjs腳本及C++代碼之上的kag腳本庫(與 NScripter類似的命令行腳本,通過TJS腳本解釋器與KAG解釋器混合執行)這三大部分組成。


簡單點說,Krkr2在覈心程序上運行着兩個有嵌套關係,但關係又不那麼緊密的解釋器,以一種不完全的,近似於模擬器上跑模擬器的形式存在着。雖然這種方式在 Windows環境中運行AVG遊戲,甚至更復雜一點的遊戲(比如RPG)都不會存在太大問題(現代CPU的速度優勢)。然而,一旦遷移到手機或智能機環境中,這種方式所帶來的後患將相當嚴重,龐大的資源損耗,將直接影響程序的運行效果(沒見過模擬器跑模擬器多恐怖的朋友,可在Windows系統下運行 android 3.0模擬器體驗類似的“速度感”PS:題外話,Android模擬器截至到3.0爲止,在PC機上的運行速度一直逆增長,版本越高速度越慢,用 android1.5反而會比3.0快很多)。

綜上所述,大家應該可以發現,如果想完整移植Krkr2遊戲的運行環境到Android系統當中,將會是一件多麼費力,而且很可能未必討好的事情,就算有高人能完整的重構整個Krkr2項目到Android系統,最終也未必會在Android系統(或其它什麼智能機平臺)跑出“人類能夠接受”的遊戲速度來。

更簡單的講,完整移植Krkr2的難度相當之大,沒能力者肯定做不到跨平臺完整移植Krkr2。而某些有能力者呢?大約也沒人會窮數月乃至數載之功,一絲不苟得做完Krkr2的完美移植,卻平白開源出來給不相識的人免費使用(特別是原作者都沒做到跨平臺的時候……)。

所以,想和ONS一樣近乎不改變任何遊戲資源,直接在Android上運行現有NS遊戲的美夢——是絕對不可能實現的。

在Android中實現Krkr2遊戲移植是可行的


事實上,如果大家曾注意Android Market的AVG遊戲動向,就會發現除了ONS的移植作品之外,還會有一些其它形式的AVG移植作品存在。但是,如果我們解壓其APK,卻很難發現諸如nscript.dat、arc.nsa這樣明顯的,和原版遊戲同樣的資源包存在。

那麼,他們是怎麼做到的呢?很簡單,他們所採用的手段是真正意義上的遊戲移植,而非ONS那樣近似於遊戲模擬器的重構了整個NS運行環境。

以Krkr2遊戲移植爲例,目前Android市場上可見的吉里吉里移植作品,大多僅僅模擬出了最容易實現的KAG3腳本部分,而無視TJS2腳本的存在,至於相應功能,則使用LUA腳本替代或者直接Java編碼補全。

這樣做雖然遠沒ONS移植省力,但在Krkr2運行環境幾乎不可能在智能機完整再現的如今,也總比自己強行重構Krkr2,再來用它運行遊戲要高效得多了。

——至少目前已知的,所有Krkr2遊戲的Android移植項目,都是這樣開發出來的。

而下文介紹的KAS項目,就是一個非常典型的Krkr2-Android版移植實現。

項目地址:http://studiomikan.net

源碼下載:http://studiomikan.net/kas/

KAS參考Krkr2腳本實現,可以部分支持KAG3腳本(標準KAG腳本命令大約有150個左右,截止到6月23日的KAS中實現了不足1/3,而且完全不支持TJS腳本),腳本命名方式同樣是.ks爲後綴。

PS:突然發現一篇博文不可能寫全,所以緊急收筆,有時間小弟將單獨介紹KAS。下面是小弟貼出的一個KAS中TagHandlers類(翻譯後的),其中包含了KAS目前所支持的全部標準KAG指令,另外下文有部分翻譯後的KAS項目工程下載地址。

*****************

package net.studiomikan.kas;

import java.util.HashMap;
import java.util.TreeMap;

// 標籤對象集合(KAS的腳本解析方式是將所有同指令的命令都放在一個集合中,所以有此接口)
interface TagObject
{
	public void run(HashMap<String, String> elm);
}

// 標籤集合處理器
public class TagHandlers
{
	private TreeMap<String, TagObject> tagHandlersMap = null;
	private MainSurfaceView owner;

	public TagHandlers(MainSurfaceView owner)
	{
		tagHandlersMap = new TreeMap<String, TagObject>();
		getTagHandlers(owner);
	}

	// 返回指定標籤名下屬的所有腳本集合
	public TagObject getTagObject(String tagName)
	{
		return tagHandlersMap.get(tagName);
	}

	// 當傳遞的MainSurfaceView不爲null時,則根據MainSurfaceView中的腳本數據構建出完整的標籤樹
	public void getTagHandlers(MainSurfaceView owner)
	{
		if(owner == null)
			return;
		this.owner = owner;
		getTagHandlers();
	}

   //PS:KAS並沒有完整的實現KAG腳本,下面給出的腳本指令不足標準KAG的1/3
	public void getTagHandlers()
	{
		// s
		// 停止執行腳本
		tagHandlersMap.put("s",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_s(); }});
		// wait
		// 暫停
		tagHandlersMap.put("wait",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_wait(elm); }});
		// r
		// 換行
		tagHandlersMap.put("r",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_r(); }});
		// er
		// 消去消息層的文字
		tagHandlersMap.put("er",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_er(); }});
		// ct
		// 重構消息層MessageLayer
		tagHandlersMap.put("ct",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_ct(); }});
		// cm
		// 清空全部消息層中的文字
		tagHandlersMap.put("cm",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_cm(); }});
		// p
		// 顯示完一頁的消息後必須點擊屏幕才允許換頁
		tagHandlersMap.put("p",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_p(); }});
		// l
		// 文字顯示到行末時必須點擊屏幕才允許換行
		tagHandlersMap.put("l",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_l(); }});
		// nowait
		// 瞬間顯示出全部文字
		tagHandlersMap.put("nowait",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_nowait(); }});
		// endnowait
		// 結束瞬間顯示全部文字模式
		tagHandlersMap.put("endnowait",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_endnowait(); }});
		// position
		// 消息層的屬性設置
        /* 參數:
               (1)對象層位置:layer = message/message0/message1
               (2)頁面類型:page = fore/back
               (3)層是否可見:visible = true/false
               (4)層的顏色:color = 整型色彩值
               (5)不透明度:opacity = 0(完全透明)~255(完全不透明)
               (6)層左端位置:left = 0 以上整數
               (7)層上端位置:top = 0 以上整數
               (8)寬:width = 0 以上整數
               (9)高:height = 0 以上整數
               (10)Layer圖像:frame = "Layer名"
               (11)Layer圖像的透明色:framekey = adapt/整型色彩值
               (12)左方邊距:marginl = 0 以上整數
               (13)上方邊距:margint = 0 以上整數
               (14)右方邊距:marginr = 0 以上整數
               (15)下方邊距:marginb = 0 以上整數
               (16)縱向書寫模式:vertical = false(默認)/true
               (17)是否允許用鼠標拖動:draggable = false(默認)/true
        */
		tagHandlersMap.put("position",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_position(elm); }});
		// layopt
		// 標準層的屬性設定
		/* 參數:
            (1)對象層位置:layer = message/message0/message1/0/1/2……
            (2)頁面類型:page = fore/back
            (3)層是否可見:visible = true/false
            (4)層左端位置:left = 0以上整數
            (5)層上端位置:top = 0以上整數
            (6)不透明度:opacity = 0(完全透明)~255(完全不透明)
            (7)消息層是否需要隱藏:autohide = true/false
            (8)重疊順序:index = 0以上整數
		*/
		tagHandlersMap.put("layopt",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_layopt(elm); }});
		// image
		// 讀取圖像
		tagHandlersMap.put("image",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_image(elm); }});
		// jump
		// 跳轉到指定腳本腳本
		/* 參數:
              (1)要跳轉到的腳本文件:storage="文件名.ks"
              (2)要跳轉到的標籤名:target="*標籤名"
              (3)是否將這個跳轉之後的部分看作“已讀”:countpage=false(默認)/true
		*/
		tagHandlersMap.put("jump",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_jump(elm); }});
		// call
		// 跳轉到指定腳本文件裏並執行相應標籤
        /* 參數:
              (1)要跳轉到的腳本文件:storage="文件名.ks"
              (2)要跳轉到的標籤名:target="*標籤名"
              (3)是否將這個跳轉之後的部分看作“已讀”:countpage=false(默認)/true
		*/
		tagHandlersMap.put("call",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_call(elm); }});
		// return
		// 返回指定的腳本位置
		/* 參數:
              (1)要跳轉到的腳本文件:storage="文件名.ks"
              (2)要跳轉到的標籤名:target="*標籤名"
              (3)是否將這個跳轉之後的部分看作“已讀”:countpage=false(默認)/true
		*/
		tagHandlersMap.put("return",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_return(); }});
		// current
		// 指定當前操作的消息層
        /* 參數:
              (1)層名:layer = message/message0/message1
              (2)頁面:page = fore(默認)/back
              (3)是否需要同時寫入背景頁面(BackPage)中:withback = false/true
		*/
		tagHandlersMap.put("current",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_current(elm); }});
		// trans
		// 命令遊戲層進行漸變轉換(這個漸變效果是根據分解漸變圖像素而生成的,所以在Android模擬器中很慢……)
		/* 參數:
              (1)漸變轉換的時間:time = 0 以上整數(毫秒)
              (2)漸變轉換的類型:method = universal(自定義,默認)/ crossfade(淡入淡出)
              (3)對象層:layer = message/message0/message1/base/0/1/2
              (4)是否包含子層:children = true(默認)/false
              (5)轉換規則圖像(僅當method = universal時有效):rule = "圖像文件名"
              (6)模糊程度值(僅當method = universal時有效):vague = 0 以上整數
              (7)捲動方向(僅當method = scroll時有效):from = left/top/right/bottom
		*/
		tagHandlersMap.put("trans",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_trans(elm); }});
		// backlay
		// 將層的前景頁面信息複製到背景頁面中
		tagHandlersMap.put("backlay",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_backlay(elm); }});
		// playbgm
		// 播放背景音樂
		/* 參數:
             (1)音效文件:storage="音效文件名"
             (2)是否循環播放:loop=false(默認)/true
             (3)音效緩衝編號:0(默認)/1/2
		*/
		tagHandlersMap.put("playbgm",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_playbgm(elm); }});
		// bgmopt
		// 設定背景音樂的屬性
		/* 參數:
             (1)音量的百分數:volume = 0~100
             (2)最大音量百分數:gvolume = 0~100
		*/
		tagHandlersMap.put("bgmopt",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_bgmopt(elm); }});
		// stopbgm
		// 停止播放背景音樂
		tagHandlersMap.put("stopbgm",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_stopbgm(elm); }});
		// playse
		// 播放音效
		/* 參數:
             (1)音效文件:storage = "音效文件名"
             (2)是否循環播放:loop = false(默認)/true
             (3)音效緩衝編號:0(默認)/1/2
		*/
		tagHandlersMap.put("playse",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_playse(elm); }});
		// seopt
		// 音效設定
		/* 參數:
             (1)音效緩衝編號:buf = 0(默認)/1/2
             (2)音量:volume = 0~100(%)
		*/
		tagHandlersMap.put("seopt",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_seopt(elm); }});
		// stopse
		// 停止播放音效
		/* 參數:
             (1)音效緩衝編號:buf = 0(默認)/1/2
		*/
		tagHandlersMap.put("stopse",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_stopse(elm); }});
		// quake
		// 畫面震動
		/* 參數:
             (1)震動的時間:time = 0 以上整數(毫秒)
			 (2)time屬性的單位:timemode = ms/delay
             (3)橫向的最大振幅:hmax = 0以上整數
             (4)縱向的最大振幅:vmax = 0以上整數
		*/
		tagHandlersMap.put("quake",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_quake(elm); }});
		// stopquake
		// 停止畫面震動
		tagHandlersMap.put("stopquake",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_stopquake(); }});
		// wq
		// 等待畫面震動停止
		tagHandlersMap.put("wq",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_wq(elm); }});
		// hidemessage
		// 隱藏消息層
		tagHandlersMap.put("hidemessage",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_hidemessage(); }});
		// gotostart
		// 回到startanchor標籤指定的地點
		/* 參數:
             (1)是否需要確認:ask=false(默認)/true
		*/
		tagHandlersMap.put("gotostart",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_gotostart(elm); }});
		// link
		// 外部鏈接
		/* 參數:
           (1)要跳轉到的腳本文件:storage = "文件名.ks"
           (2)要跳轉到的標籤名:target = "*標籤名"
           (3)鏈接色:color = 顏色值
           (4)鼠標進入時的音效:enterse = "音效文件名"
           (5)鼠標進入時的音效的緩衝編號:entersebuf = 0/1/2
           (6)鼠標點擊時的音效:clickse = "音效文件名"
           (7)鼠標點擊時的音效的緩衝編號:clicksebuf = 0/1/2
           (8)鼠標退出時的音效:leavese = "音效文件名"
           (9)鼠標退出時的音效的緩衝編號:leavesebuf = 0/1/2
           (10)是否將這個跳轉之後的部分看作“已讀”:countpage = true(默認)/false
		*/
		tagHandlersMap.put("link",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_link(elm); }});
		// endlink
		// 結束鏈接
		tagHandlersMap.put("endlink",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_endlink(); }});
		// locklink
		// 鎖定鏈接
		tagHandlersMap.put("locklink",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_locklink(); }});
		// unlocklink
		// 解鎖鏈接
		tagHandlersMap.put("unlocklink",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_unlocklink(); }});
		// deffont
		// 設定默認的文字屬性
        /* 參數:
             (1)字體名:face = "字體名"
             (2)文字大小:size = 0以上整數
             (3)文字顏色:color = 顏色值
             (4)是否粗體:bold = false(默認)/true
             (5)是否描邊:edge = false(默認)/true
             (6)描邊顏色:edgecolor = 顏色值
             (7)是否顯示陰影:shadow = true(默認)/false
             (8)陰影顏色:shadowcolor = 顏色值
		*/
		tagHandlersMap.put("deffont",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_deffont(elm); }});
		// defstyle
		// 設定默認的文字風格
		/* 參數:
             (1)字間距:pitch = 0 以上整數
             (2)行間距:linespacing = 0 以上整數
             (3)列間距:linesize = 0 以上整數
		*/
		tagHandlersMap.put("defstyle",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_defstyle(elm); }});
		// laycount
		// 更改層的數量
		/* 參數:
             (1)普通層的數量:layers = 0 以上整數
             (2)消息層的數量:messages = 1 以上整數
		*/
		tagHandlersMap.put("laycount",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_laycount(elm); }});
		// fadeoutbgm
		// 淡出背景音樂
		tagHandlersMap.put("fadeoutbgm",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_fadeoutbgm(elm); }});
		// fadeoutse
		// 淡出音效
		tagHandlersMap.put("fadeoutse",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_fadeoutse(elm); }});
		// fadeinbgm
		// 淡入背景音樂
		tagHandlersMap.put("fadeinbgm",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_fadeinbgm(elm); }});
		// fadeinse
		// 淡入音效
		tagHandlersMap.put("fadeinse",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_fadeinse(elm); }});
		// move
		// 移動層
	    /* 參數:
             (1)對象層:layer = message/message0/message1/0/1/2……
             (2)移動的位置:path = "(x1,y1,opacity1)(x2,y2,opacity2)……"
             (3)到達一個點 (x,y,opacity) 所用的時間:time = 0以上整數(毫秒)
             (4)頁面:page = fore/back
             (5)加速度:accel = 0(默認)/正數/負數
             (6)動作開始前的延遲時間:delay = 0以上整數(毫秒)
		*/
		tagHandlersMap.put("move",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_move(elm); }});
		// load
		// 載入存檔
		/* 參數:
             (1)保存的位置編號:place = 0 以上整數(0爲默認)
             (2)是否需要確認:ask = false(默認)/true
		*/
		tagHandlersMap.put("load",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_load(elm); }});
		// save
		// 保存存檔
		/* 參數:
             (1)保存的位置編號:place = 0 以上整數(0爲默認)
             (2)是否需要確認:ask = false(默認)/true
		*/
		tagHandlersMap.put("save",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_save(elm); }});


		//------------------------------------------------------------------------------------------
		// 系統操作指令

		// autowc
		// 自動等待
		/* 參數:
             (1)自動等待輸入的文字:ch = "文字對象"
             (2)自動等待是否有效:enabled = true/false
             (3)等待時間:time = 0 以上整數
		*/
		tagHandlersMap.put("autowc",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_autowc(elm); }});
		// clearsysvar
		// 消除所有系統變量
		tagHandlersMap.put("clearsysvar",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_clearsysvar(); }});
		// clickskip
		// 在點擊畫面時允許跳過當前腳本進度
		tagHandlersMap.put("clickskip",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_clickskip(elm); }});
		// close
		// 腳本結束
		tagHandlersMap.put("close",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_close(elm); }});
		// title
		// 指定標題
		tagHandlersMap.put("title",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_title(elm); }});
		// waitclick
		// 在點擊畫面後才能繼續進行腳本解析
		tagHandlersMap.put("waitclick",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_waitclick(); }});
		// wc
		// 設定文字顯示間隔時間
		tagHandlersMap.put("wc",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_wc(elm); }});

		//------------------------------------------------------------------------------------------
		// 宏指令

		// erasemacro
		// 清除宏
		tagHandlersMap.put("erasemacro",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_erasemacro(elm); }});

		//------------------------------------------------------------------------------------------
		// 自動閱讀模式

		// cancelautomode
		// 解除“自動閱讀”模式
		tagHandlersMap.put("cancelautomode",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_cancelautomode(); }});
		// cancelskip
		// 解除跳過模式
		tagHandlersMap.put("cancelskip",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_cancelskip(); }});
		// ch
		// 顯示文字
		tagHandlersMap.put("ch",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_ch(elm); }});
		// indent
		// 設定文字縮進
		tagHandlersMap.put("indent",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_indent(); }});
		// endindent
		// 解除文字縮進
		tagHandlersMap.put("endindent",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_endindent(); }});
		// locate
		// 指定文字的顯示位置
		/* 參數:
             (1)橫方向位置:x = 0 以上整數
             (2)縱方向位置:y = 0 以上整數
		*/
		tagHandlersMap.put("locate",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_locate(elm); }});
		// delay
		// 文字顯示速度
		tagHandlersMap.put("delay",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_delay(elm); }});

		//------------------------------------------------------------------------------------------
		// 歷史數據

		// clearhistory : KAS專有,非標準KAG腳本語句
		// 清空歷史數據
		tagHandlersMap.put("clearhistory",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_clearhistory(); }});
		// history
		// 歷史設定
		/* 參數:
             (1)歷史是否可顯示:enabled=true/false
             (2)是否輸出文字信息至歷史:output=true/false
		*/
		tagHandlersMap.put("history",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_history(elm); }});
		// hr
		// 歷史的換行/換頁
		/* 參數:
             (1)換行是否需要換頁:repage=false(默認)/true
		*/
		tagHandlersMap.put("hr",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_hr(elm); }});
		// showhistory
		// 顯示歷史數據
		tagHandlersMap.put("showhistory",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_showhistory(); }});


		//------------------------------------------------------------------------------------------
		// 跳轉指令

		// button
		// 圖形按鈕(PS:幾乎沒有實現標準KAG腳本button標籤應有的功能……)
		tagHandlersMap.put("button",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_button(elm); }});


		//------------------------------------------------------------------------------------------
		// 層指令

		// copylay
		// 複製層
		/* 參數:
            (1)複製源層:srclayer = message/message0/message1/base/0/1/2……
            (2)複製目標層:destlayer = message/message0/message1/base/0/1/2……
            (3)複製源頁面:srcpage = fore/back
            (4)複製目標頁面:destpage = fore/back
		*/
		tagHandlersMap.put("copylay",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_copylay(elm); }});
		// freeimage
		// 釋放圖像層資源
		tagHandlersMap.put("freeimage",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_freeimage(elm); }});
		// stopmove
		// 停止層的移動
		tagHandlersMap.put("stopmove",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_stopmove(); }});
		// stoptrans
		// 停止層的漸變轉換
		tagHandlersMap.put("stoptrans",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_stoptrans(); }});
		// wm
		// 等待層自動移動結束
		tagHandlersMap.put("wm",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_wm(elm); }});

		//------------------------------------------------------------------------------------------
		// 音效指令

		// fadebgm
		// 淡入背景音樂
		tagHandlersMap.put("fadebgm",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_fadebgm(elm); }});
		// fadese
		// 淡入音效音樂
		tagHandlersMap.put("fadese",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_fadese(elm); }});
		// pausebgm
		// 暫停背景音樂
		tagHandlersMap.put("pausebgm",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_pausebgm(); }});

		// resumebgm
		// 繼續播放背景音樂
		tagHandlersMap.put("resumebgm",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_resumebgm(); }});
		// wb
		// 等待背景音樂淡出
		tagHandlersMap.put("wb",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_wb(elm); }});
		// wf
		// 等待音效淡出
		tagHandlersMap.put("wf",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_wf(elm); }});
		// wl
		// 等待背景音樂播放結束
		tagHandlersMap.put("wl",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_wl(elm); }});
		// ws
		// 等待音效播放結束
		tagHandlersMap.put("ws",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_ws(elm); }});

		//------------------------------------------------------------------------------------------
		// 變量指令(PS:由於沒有TJS解釋器,所以TJS相關部分不具備)

		// clearvar
		// 清理所有遊戲變量
		tagHandlersMap.put("clearvar",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_clearvar(); }});
		// emb
		// 顯示TJS腳本的執行結果(在KAS中僅僅支持四則運算罷了)
		tagHandlersMap.put("emb",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_emb(elm); }});

		//------------------------------------------------------------------------------------------
		// 存檔指令

		// copybookmark
		// 複製存檔
		tagHandlersMap.put("copybookmark",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_copybookmark(elm); }});
		// disablestore
		// 禁止存檔
		tagHandlersMap.put("disablestore",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_disablestore(elm); }});
		// erasebookmark
		// 刪除存檔
		tagHandlersMap.put("erasebookmark",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_erasebookmark(elm); }});
		// startanchor
		// 即時存檔,同時也是gotostart指令的返回點
		tagHandlersMap.put("startanchor",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_startanchor(elm); }});
		// store
		// 開啓存檔功能
		tagHandlersMap.put("store",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_store(elm); }});

		// tempload
		// 讀取內存上的臨時存檔
		tagHandlersMap.put("tempload",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_tempload(elm); }});
		// tempsave
		// 保存臨時存檔到遊戲內存
		tagHandlersMap.put("tempsave",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_tempsave(elm); }});


		//------------------------------------------------------------------------------------------
		// 變量指令

		// assign : KAS專有指令,非標準KAG腳本
		// 將指定數值插入遊戲變量中
		tagHandlersMap.put("assign",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_assign(elm); }});
		// fillcolorbase : KAS專有指令,非標準KAG腳本
		// 填充北京爲指定顏色
		tagHandlersMap.put("fillcolorbase",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_fillcolorbase(elm); }});
		// setversion : KAS專有指令,非標準KAG腳本
		// 設定遊戲版本
		tagHandlersMap.put("setversion",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_setversion(elm); }});
		// callload : KAS專有指令,非標準KAG腳本
		// 呼出遊戲讀檔界面
		tagHandlersMap.put("callload",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_callload(); }});
		// callsave : KAS專有指令,非標準KAG腳本
		// 呼出遊戲存檔界面
		tagHandlersMap.put("callsave",
		new TagObject(){ public void run(HashMap<String, String> elm){ owner.tag_callsave(); }});
		// printlayerstate : 打印出遊戲狀態(後臺執行)
		tagHandlersMap.put("printlayerstate",
		new TagObject(){ public void run(HashMap<String, String> elm){
			owner.tag_debug_printlayerstate(elm);
		}});

	}

}


*****************

此外,KAS默認的屏幕大小爲800x450。

作者給出的效果圖:



模擬器效果:
(KAS採用Canvas渲染,在模擬器與不支持硬件加速的手機中速度會比ONS更快,缺點是Canvas在圖像縮放時畫質損失較大)



改變KAS的Util類中布爾值debug可以設定是否開啓調試模式(原版默認爲false,小弟修正包中默認true)

對了,在iPhone下有同類項目Artemis Engine,兩者功能上基本一致(都是僅支持最基本的KAG腳本,與標準Krkr2最典型的差異是不能識別TJS文件以及不支持@iscript標記,也就是都沒有提供支持TJS腳本解釋器),有興趣的朋友可以看看。

如果原版ONS或KAS運行時出現問題的話(比如無法直接顯示遊戲畫面),也可下載小弟提供的修正版本(加入NS資源打包工具,給ONS加上了編譯好的so 文件,修正成可讀取assets文件夾下游戲資源(注意讀取assets目錄下壓縮文件的大小限制)。給KAS加入部分中文註釋,修正了原版的 Canvas設定,真機上大約比原版快12FPS左右,在畫面縮放時稍微比原版清晰些)。


下載地址:http://download.csdn.net/source/3516651

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章