跳到主要內容

千字文射擊 BreakShoot

[GitHub][Play]

兒子小學低年級時,學校要求他們背弟子規,所以我想如果他們也能學習千字文應該很不錯。千字文總共一千個字,每個字都不同,是古代的兒童課本。學習千字文可以了解古代社會的規範、歷史常識和禮節禮儀等。平常只要有機會我就會想辨法引導他們去讀讀千字文,這也是製作這個小遊戲的用意。





動態點陣字標題

遊戲的標題是由'BREAKSHOOT'這幾個字元構成,每一個字元的畫素又以千字文裡的一個中文字取代。進入標題畫面時,可以看到有約1秒鐘的動畫,每一個點陣字的中文字畫素隨機打散至畫面四周,然後再飛入重組。

點陣字的畫素取得是先透過Graphics.GenCanvas生成一個大小9x18的虛擬畫布,然後每次從'BREAKSHOOT'裡面取出一個個字元出來,以Graphics.DrawText畫到這個畫布上。之後再以Graphics.GetPixel取出畫布上的每個點,如果不是0就生一個中文字出來。

點陣字的動畫則是把上面的步驟裡生出來的中文字由STGE腳本來控制移動。為了實現這個功能,新增了一條API:Stge.LoadScript,可以在Runtime建立新的腳本。整個動作結合起來,如下面程式所示:

local function LoadStgeCharScript(c, W, H, script_name, ch)
  Graphics.FillRect(c, 0, 0, W, H, 0)
  Graphics.DrawText(c, 0, 0, ch, 1)
  local script = 'script ' .. script_name
  for x= 0, W-1 do
    for y= 0, H-1 do
      local p = Graphics.GetPixel(c, x, y)
      if (0 ~= p) then
        script = script .. string.format(' fire(title_char,$1+%d,$2+%d)', CW * x, CH * y)
      end
    end
  end
  script = script .. ' end'
  Stge.LoadScript(script)
end

每次呼叫一次LoadStgeCharScript就可以動態產生一段STGE腳本對應到'BREAKSHOOT'裡面的一個字元,腳本的名稱是char_1~char_10。之後會再一個個呼叫它們,讓每個字元動起來,如下所示:

local function LoadStgeTitleScript(c, W, H, script_name, text)
  for i = 1, #text do
    LoadStgeCharScript(c, W, H, 'char_' .. i, string.sub(text,i,i))
  end
  local script = string.format('script %s sleep(0.2)', script_name)
  local offsetx = (SW - #text * W * CW) / 2 + CW/2
  for i = 1, #text do
    local x = math.floor(offsetx + (i - 1) * W * CW - SW/2)
    script = script .. string.format(' fork(char_%d,%d,%d)', i, x, -100)
  end
  script = script .. ' sleep(1) userdata(1) fire(title_hint) end'
  Stge.LoadScript(script)
end

每一個字元裡的畫素是以title_char來控制關聯中文字的位置,先是隨機飛到畫面周邊,然後再飛回它的定位。每一個點陣字的畫素的定位座標,是在LoadStgeCharScript裡面就己經事先指定好。title_char的定義如下:

script title_char
  changex(rand(0.2,0.4),rand(-$w/2,$w/2))
  changey(rand(0.2,0.4),rand(-$h/2,$h/2))
  sleep(0.4)
  changex(rand(0.1,0.5),$1)
  changey(rand(0.1,0.5),$2)
  sleep(0.5)
end

標題畫面初始化時會以Stge.RunScript執行以上腳本來產生動態點陣字,若是無法執行則表示是第一次初始化需要動態產生腳本。如下所示:

local script_name = 'title_text'
if (-1 == Stge.RunScript(script_name)) then
  local CW5x8,CH5x8 = 5,8
  local c = Graphics.GenCanvas(CW5x8, CH5x8)
  Graphics.SetFont(Graphics.FIXED_FONT)
  LoadStgeTitleScript(c, CW5x8, CH5x8, script_name, 'BREAKSHOOT')
  Graphics.KillCanvas(c)
  Graphics.SetFont(Graphics.SYSTEM_FONT)
  Stge.RunScript(script_name)
end

最後再建立每一個畫素粒子和中文字的關聯就完成了。

多層星空的產生及捲動

遊戲中每一個畫面都以簡單的星空作背景,在純黑色的背景疊上3層星星,點上2x2的小白點作為星星,越遠的星星顏色越淡,畫面捲動時也移動的越慢。

3層星星是以STGE腳本生成的。如下:

script star
  repeat(32)
    userdata(3,0.9,0.9,0.9)
    fire(star_a)
    userdata(2,0.5,0.5,0.5)
    fire(star_a)
    userdata(1,0.3,0.3,0.3)
    fire(star_a)
  end
end

script star_a
  changex(0,rand(-$w/2,$w/2))
  changey(0,rand(-$h/2,$h/2))
end

可以看到star裡的repeat迴圈在每一層星空裡共生出32個星星,userdata的第一個參數用來控制這是第幾層的星星,後面3個參數是用來指定星星的RGB值,至於star_a只是用來隨機設定星星的位置。

當畫面捲動時,以Lua函式MoveStars來捲動3層星空。如下:

function MoveStars(stars, x)
  local offset = (x - SCR_W/2) / SCR_W
  for i = 1, #stars do
    Good.SetPos(stars[i], 8 * i * offset, 0)
  end
end

stars是事先己經根據不同層(userdata第一個參數)儲存好的3層星星群的父物件。x是目前的畫面捲動位置,依據畫面的x座標相對於畫面中心計算出偏移值,再由這個偏移值去移動所有星星。迴圈的i就是第幾層的星空索引,x8可以讓不同層的星星移動速度有所區別。這樣就作到越遠的星星動的慢,越近的星星動的快。

顯示注音

之前看到老婆在幫女兒作上小學前的注音預習,當時突然想到或許可以寫個小工具讓她使用,可以幫忙生出簡單的注音小測驗題。所以那時候利用小麥注音輸入法的國字和注音的對照表寫了一個國字轉注音的小測試程式,但後來就沒有繼續往下作。因為我沒有處理詞彙,所以會把一字多音都列出來。如下圖:



注音小工具的HTML網頁原始碼如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {font-family:Helvetica, Arial, "黑體-繁", "微軟正黑體", sans-serif}
</style>
</head>
<body>
<input type="text" id="inp">
<input type="button" value="國字轉注音" onclick="doParse()">
<div id="res"></div>
<script>
var dict = [];

fetch('BPMFBase.txt').then(function(response) {
  if (response.ok) {
    return response.text();
  } else {
    throw Error(response.statusText);
  }
}).then(function(text) {
  var lines = text.split("\n");
  lines.map(function(line) {
    var parts = line.split(' ');
    if (2 <= parts.length) {
      var c = parts[0], zhuyin = parts[1];
      var a = dict[c];
      if (null != a) {
        a.push(zhuyin);
      } else {
        dict[c] = [zhuyin];
      }
    }
  });
}).catch(function(error) {
  alert(error);
});

function doParse() {
  var s = document.getElementById('inp').value;
  var chars = s.split('');
  var html = '';
  for (var i = 0; i < chars.length; i++) {
    var ch = chars[i];
    html += ch;
    var a = dict[ch];
    if (null != a) {
      html += ' ' + a;
    }
    html += '<br>';
  }
  document.getElementById('res').innerHTML = html;
}
</script>
</body>

製作千字文射擊時利用這個小工具,把一千個字輸入,再把輸出結果存檔為zhuyin.txt。zhuyin.txt的內容是一行一個字和它的注音,如下:

天 ㄊㄧㄢ
地 ㄉㄧˋ
玄 ㄒㄩㄢˊ
...

手工把一字多音的部份都處理好後,就可以把注音對照表輸入到Lua程式裡使用了。如下:

local STR1000 = '天地玄...'
local STR1000_ZHUYIN = {[1]='ㄊㄧㄢ',[2]='ㄉㄧˋ',[3]='ㄒㄩㄢˊ',...

STR1000_ZHUYIN總共有一千組注音,當然不是用手工一個一個輸入,而是用一小段PHP來作,如下:

<?php
$lines = explode("\r\n", file_get_contents('zhuyin.txt'));
$no = 1;
foreach ($lines as $line) {
  echo "[$no]='".explode(' ', $line)[1]."',";
  $no += 1;
}

前置作業都準備好後,剩下的就是在遊戲裡面把注音畫出來,這裡就不再贅述了。

留言

這個網誌中的熱門文章

以lex/yacc實作算式計算機

前面我們透過 手工的方式 實作了一個簡易的算式計算機,現在我們要開始使用工具來作同樣的事,比較看看手工和使用工具有什麼不同的差別。首先要介紹的就是lex&yacc。 lex & yacc lex(Lexical Analyzar)及yacc(Yet Another Compiler Compiler)是用來輔助程式設計師製作語法剖析器的程式工具。lex的工作就是幫助我們將輸入的資料文字串流分解成一個個有意義的token,而yacc的工作就是幫我們分析這些token和我們定義的規則作匹配。下圖中所表示的是使用lex及yacc的一般工作流程。 首先看到yacc會讀入一個.y檔案,這裡.y檔案的內容就是我們使用類似(E)BNF語法定義的語法規則,yacc會分析這些語法規則後,幫我們產生可以用來解析這些規則的程式碼,而這個檔案一般名稱預設為y.tab.c,產生的程式碼裡面最重要的一個的函式叫作yyparse。 同yacc類似,lex也會讀入一個.l的檔案,這個檔案裡面定義的是如何從文字流裡解出token的規則,使用的方法是常規表示式(regular expression)。在圖的左側中間我們還可以看到有一個叫作y.tab.h的檔案從yacc產生出來並餵給lex作輸入,這個檔案是yacc根據在讀入的.y檔裡面所定義的token代號所產生出來的一個header,這樣yacc及lex產生出來的程式碼裡面就可以使用共通定義的代碼而不必各寫個的。lex分析過.l檔案後也會產生一個一般預設叫作lex.yy.c的原始碼檔案,裡頭最重要的一個函式叫作yylex。 最後,我們把yacc產生出來的y.tab.c還有lex產生出來的lex.yy.c,以及其它我們自己撰寫的原始碼檔案一起拿來編譯再作連結,最後產生出來的就是一個可以用來解析我們定義的語法的解析器工具。以上是整個lex及yacc的使用流程概觀。 常規表示式 在正式使用lex之前,我們首先來對常規表示法作一個基本的認識。常規表示法是一種用來表示字串樣式(pattern)的中繼語言,就好比前文所介紹的(E)BNF表示式一樣,都是用來描述其它語言的語言,只不過用途不太一樣罷了。 常規表示式使用一些中繼符號(meta-symbol)以及ASCII字元定義字串樣式,以下列出一些常規表示式所使用的符號。 . 表示除了換行字元...

單人撲克牌遊戲 - 蒙地卡羅

更多可在網頁玩的 單人撲克牌遊戲 ; 新增一個簡單的單人撲克牌遊戲: 蒙地卡羅 ,簡單介紹一下玩法。 下載 事先排列好5x5張牌。 每次移動一張可以配對的牌,並消除這對牌。在上下、左右及斜向相隣的二張牌,只要擁有同樣數字(不計花色),即可配對。 消除二張配對的牌後,剩餘的牌以往左往上的方式補滿空隙,接著在發新牌補滿後面的空格。 重覆步驟2~3,直到沒有牌可以配對及發完所有牌為止。 結果有二種。一個是勝利,成功的消除掉所有牌。另一個是Gameover沒有牌可以再作配對。

關於C/C++的指標

我想應該還有不少人在使用指標上有些地方觀念不大清楚,比如說下面二個函式,那個是正確的?為什麼?像這樣的問題如果弄不清楚,寫出來的程式一定非常危險。 // 為簡化忽略檢查 void alloc_mem(char* p) // 版本1 {   p = new char[100]; } void alloc_mem(char** p) // 版本2 {   *p = new char[100]; } 如上,這個函式要配置大小是100個字元的記憶體並從傳入的參數p回傳,這二個版本除了輸入參數不一樣外大致上是一樣的;從第一個版本來看,參數是一個字元指標,記憶體配置出之後直接傳給p,如果觀念正確的人一定可以馬上指出這樣的寫法是錯誤的,第二個版本才是正確能work的。 現在就來說明為什麼,在這之前先要了解在C/C++中,函式的參數是如何傳遞的,在C/C++中函式的呼叫所傳入的參數是透過堆疊(Stack) 來傳入函式的,不懂什麼是堆疊也沒關係,就把它看成是另外一塊記憶體也行,當在程式中呼叫某個函式時,傳入的參數會先被複製到這塊記憶體中,當在函式中要使用這些參數時再從堆疊中去取出來。 以版本1的例子來說明,如下在程式中大概會這樣呼叫。 char* pp = NULL; alloc_mem(pp); pp一開始的初值是NULL,當呼叫alloc_mem時,pp的值會被複製到堆疊中(傳址),這種情況和以下的code事實上是對等的,只不過 p的值一開始被初始化成和pp的值一樣,p就好像一個區域變數一樣,一離開函式後這個變數就無效了,所以在外面的pp的值永遠都不會改變,同時在涵式中 new出來的記憶體也lost掉了。 void alloc_mem() {   char* p = new char[100]; } 再來看版本2,它的參數是一個指標的指標,這是什麼意思,我們先從實際使用上來看,如下。 char* pp = NULL; alloc_mem(&pp); 這次我們把pp這個變數的位址傳入涵式,所以在涵式中所得到的是pp這個變數的位址,在函式中p所含的內容是pp的位址,pp是一個char*形態的變數,p是一個指標它的內容是char*的形態,現在p已經指向pp了,所以對p的內容作改變,相對的pp的值也會跟著改變。 現在來看另...