千字文射擊

«Home / 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;
}

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