跳到主要內容

千字文射擊 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;
}

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

留言

這個網誌中的熱門文章

猜數字遊戲 (電腦猜人)

前幾天午睡時突然被告知要參加公司內部的程式設計比賽,題目是用C寫一支文字模式的4位數字猜數字遊戲,由使用者來猜電腦的數字。在上星期時其實就已經有公佈了,但我沒有注意到所以是臨時加入,還好這是個簡單的題目,不用花多少時間就可以寫出來。 規則: - 這是一對一比賽,雙方各選擇一4位數字,不讓對方知道。 - 4位數字由數字0至9組成,每位數不得重複。 - 雙方輪流猜對方的數字,直到一方猜中為止。 - A方猜B方的數字後,B方根據A方的猜測回答幾A幾B。 - 一個A表示猜中一個數字且位置正確,一個B表示猜中一個數字但位置不正確。 - 當一方猜中4A0B時即表示猜中對方全部4個數字且位置正確,贏得比賽。 - 例:B的謎底是4208,底下箭頭左測是A的猜測,箭頭右測是B的回答。    1234 ==> 1A1B    5678 ==> 1A0B    2406 ==> 1A2B    ...    4208 ==> 4A0B ; 寫個程式讓玩家來猜電腦的數字不難,不過我從來沒有寫過讓電腦來猜玩家數字的版本,所以花了點時間想想怎麼寫。 研究後歸納出二個點。 1, 使用窮舉法將所有可能數字組合列出。 2, 每次猜測後根據結果排除不可能是答案的組合,重複這個動作直到猜中答案為止。 第1點只是實作問題,第2點概念也很簡單,但要過濾不是答案的組合根據的是什麼?乍看之下沒什麼頭緒,不過想通之後就非常簡單了。 它的基本原理如下:假如謎底是4561,如果猜1524則會得到1A2B。從相反的角度來看,如果謎底是1524,則猜4561時也會得到1A2B的回答。 利用這個方法,每一次猜測一個數字X後,再以這個數字當作答案,來和所有剩下來的候選答案作比對,如果得到的結果(幾A幾B)和數字X是一樣的話,就把這個數字保留下來繼續作為候選答案,否則就過把這個數字過濾掉。下一把,繼續從候選答案裡選一個出來猜,重複上面的動作,直到猜中為止。 ; C++ STL的algorithm裡有個叫作next_permutation的函數,可以用來生成排列。 #include <iostream> #include <algorithm> using namespace std; int main () {   int myints[] = {1,2,3};  ...

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

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

以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字元定義字串樣式,以下列出一些常規表示式所使用的符號。 . 表示除了換行字元...