v0.16 - by 晴☆
就是你現在正在看的簡報網站,按 吧
右下角的箭頭可以換頁,但簡報頁面的排列方式不是網格狀的,而是像很多串投影片,而它會記住使用者在每一串投影片的垂直位置(預設是最上方),所以先往下滑到底看完一個章節,再往右滑至另一個章節吧!
Title A
Title B
Title C
Title D
A1
B1
C1
D1
A2
C2
小提示:
也可以用鍵盤的方向鍵換頁
按 [Esc] 可以看縮圖
回到標題頁可以重置簡報順序
介紹一下
根據 Lexaloffle 官方說明:
Pico-8 是一款用於製作、分享和遊玩小遊戲和其他電腦程式的奇幻遊戲機。它有自己的主機控制台,但它是在個人電腦上運行
當你打開它時,會看見命令列、一套遊戲製作工具和一個名為 Splore 的線上卡夾瀏覽器
根據我的解釋:
Pico-8 可以用來寫小遊戲
硬體受限,所以考驗創意!
Pico-8 有刻意限制過硬體規格
希望真正有趣、好玩的遊戲可以不被「不夠華麗」所拘束
| 介面 | 128x128 像素,挑選過的 16 色 |
| 卡夾尺寸 | 32 KB |
| 聲軌 | 8-bit、4 通道 |
| 程式語言 | Pico-8 加強版的 Lua |
| CPU | 4M vm insts/sec |
| Sprite | 256 個 8x8 像素的拼合圖 |
| 地圖 | 128x32 瓦片 |
話說,小遊戲的定義是什麼?
就是一些小巧的專案!
遊戲的檔案內容會以 .p8 儲存(內容是純文字)
一個小遊戲稱為 卡帶 Cartridge(簡稱 Cart)
卡帶聽起來很古老吧
就像早期遊戲主機,Pico-8 只有 6 個輸入:
[↑] [←][↓][→]
[c]&[z]
[x]&[v]
使用 Pico-8 製作遊戲吧
Pico-8 是付費產品
但購買後可以無限更新
推薦買一下!
但是有點貴……
在提到編輯介面前,必須先看得懂 Pico-8 的文字
畢竟他只有 128 像素,所以每個字必須最精簡
特別注意 M、W、Q 等字的顯示方法
預設打出來的字都是大寫,比較不會糊成一團
一個字元是 3 x 5 像素,字元之間空一像素
按著 Shift 會打出特殊符號(兩字元寬)
進到 Pico-8 應該會看到這個畫面 →
這就是 Pico-8 的 命令列 Console
直接講指令!
| save <name> | 將目前文件存檔 |
| load <name> | 讀取檔案 |
| run | 開始跑程式碼 |
| folder | 打開所有存檔所在的資料夾 |
| ls | 列出所有檔案 |
| reboot | 重啟 Pico-8 |
我們來用 Lua 寫一個大家一定寫過的……
印出 "Hello World!" 吧
print("hello world!")首先按 Esc 來到編輯介面,然後輸入:
然後再按一次 Esc 回到 Console,輸入 run 來執行!
(或者直接按 Ctrl + R)
Lua 是一種程式語言:
一些基本的語法例子:
if hungry then
eat()
endwhile hungry do
eat()
end如果餓就吃
餓的話就一直吃
repeat
eat()
until full一直吃到飽為止
不用加分號
大部分東西用 end 結尾
用 -- 註解
但是 VS Code 和 Pico-8 兩邊任何一邊有更動就一定要存檔
VS Code 存完檔後,Pico-8 要按 Ctrl + R 開始執行才會更新
Pico-8 存檔後,VS Code 會自動更新
注意:
如果要用 VS Code,可以裝這個差件:
滑鼠停在函數上時會有提示:
一個遊戲不會只有文字
我們要印一些圖形出來
試試這段程式碼:
rect(8, 8, 119, 119, 8)結果應該會像這樣 →
抱歉,這真的非常醜
所以剛才的程式碼代表「畫一個從 8, 8 到 119, 119 的 8 號顏色方形」
那什麼是 8, 8 到 119, 119?什麼又是 8 號顏色?
看到
頁面,可以找到:
(有 [ ] 括號的是選擇性參數,不一定要有)
Pico-8 的介面是 128 x 128
於是他的座標也是 128 x 128
左上角為 0, 0
往右方是 x+
往下方是 y+
問題來了,右下角是多少?
是 127, 127
因為從 0 開始!
看到編輯介面右上角
| 程式 | 圖形 | 地圖 | 音效 | 音樂 |
程式:寫程式的地方
圖形:可以編輯很多個 8 x 8 的 拼合圖 Sprite
地圖:由很多的 Sprite 排成網格狀的地圖
音效:根據音高、音色、音量與效果等等編輯單一音軌
音樂:由 4 個音軌組成可以連續播放的音樂
看到圖形編輯的介面 →
在滑鼠移動到 綠色上時,左下角有 "Colour 11"
代表綠色就是 11 號顏色!
顏色是按照順序排
不小心就記起來了
在 API 中 col 就是指顏色
還附上了這張圖 →
先解釋一下什麼是 Sprite
看到右邊這是 Slimey, Jump! 的編輯介面截圖
底下就是很多 Sprite 組成的表(Spritesheet)
Sprite 可以想像成很多的格子
每格裡面都是一張圖,把它們隨便排列在一起
可以取其中的一塊當作圖片輸出
遊戲中的角色都是一格一格的動畫
地圖也是很多 Sprite 排列在一起的
先看到底下的區塊
這裡是 Sprite 區,現在是空的
白色框框是現在正在編輯的 Sprite
不要去動這個叉叉,之後會解釋
這是正在編輯的 Sprite 的編號
之後會用到!
| 畫筆 | 印章 | 選取 | 移動 | 填滿 | 圓形 |
畫筆:畫圖
印章:會印出剪貼簿上的東西
選取:矩形選取
移動:可以自由移動
(也可以按著 Space 並拖曳鼠標)
填滿:填滿相鄰的顏色
圓形:畫出圓形
上方的滑桿可以調整畫筆粗細
下方的滑桿則是視野大小
(也可以用滑鼠滾輪調整)
一般來講,不太會調整畫筆粗細
而調整視野大小則很方便
比方說,可以很方便的畫 2 x 2 的圖片
這邊可以切換不同頁
其實它儲存的方式是連續往下的
第 1 頁在第 0 頁的下方……等
視野放到最大的時候可以跨頁!
注意:第 2 和第 3 頁是與地圖共用的
建議不要使用!
完整的地圖比更多的 Sprite 更重要
接下來就是考驗創意能力了!
註:記得存檔
這次我畫的角色:
對齊左邊的版本:
我們需要用一個函數來把剛才畫的 Sprite 印出來:
其中 n 是編號:
w & h 是 Spritesheet 上的寬和高
一個 Sprite 是 8 x 8,所以 w = 2, h = 2 時
就是 2 x 2 格 Sprite,也就是 16 x 16 像素
flip_x 和 flip_y 是翻轉(水平 / 垂直 鏡像)
在程式碼區打上:
spr(1, 8, 8)應該會得到這個畫面 →
仔細看的話會發現它在這裡:
我們需要把螢幕淨空再印東西出來
這時需要這個函數:
於是我們把程式碼改成這樣:
cls()
spr(1, 8, 8)應該會得到這個畫面 →
看起來整潔多了!
動畫格、角色的物件
剛才的程式碼只執行一次就停止了
顯然不是一個遊戲應有的樣子
這時我們需要三個函數:
function _init()
-- initialize 初始化
endfunction _update()
-- 30 frame / 1 秒的更新
endfunction _draw()
-- 繪製
end注意前面的底線喔!
Frame 翻譯成「幀」或「影格」
意思就是動畫的每一格
frame rate 越高
動畫看起來越順
螢幕並不是連續的
而是每 Frame 更新後
產生下一格並繪製到螢幕上
在程式區打上這個:
function _init()
end
function _update()
end
function _draw()
cls()
spr(1, 59, 59)
end現在應該會一直執行,按 Esc 可以中斷
我們在 _init 加入一個玩家物件,並在 _draw 讀取它的 x & y:
function _init()
-- the player object
pl = {
x = 59,
y = 59
}
end
function _update()
end
function _draw()
cls()
spr(1, pl.x, pl.y)
endpl = {
x = 59,
y = 59
}
-- 也可以寫成:
pl = {}
pl.x = 59
pl.y = 59
在 Lua 中,{ } 括號像是一個物件
它可以有自己的屬性,並且可增加屬性
如果剛才加入了 pl 沒有改變
沒什麼問題!
我們現在才要讓它動起來:
function _init()
pl = {
x = 59,
y = 59
}
end
function _update()
pl.x += 1 -- move right!
end
function _draw()
cls()
spr(1, pl.x, pl.y)
end這是輸入代碼圖:
可以用 btn 函數取得
例如想看看玩家是否正按著「上」鍵:
if btn(2) then
end其實讓玩家操作角色沒有那麼單純,有的時候是遊戲可以玩,但操作很不順暢
平時也多觀察別人做的遊戲的細節吧
把 _update 改成這樣:
function _update()
if btn(0) then
pl.x -= 1
end
if btn(1) then
pl.x += 1
end
if btn(2) then
pl.y -= 1
end
if btn(3) then
pl.y += 1
end
end加入牆壁吧!
我在 64 號 Sprite 畫了牆壁:
(第 1 頁第 1 個)
進到地圖編輯畫面,畫一個畫面的框框
一個畫面是 128 x 128 像素
一個 Sprite 是 8 x 8
所以一個畫面有 16 格地圖,也就是 1~15 格
可以從左下角的座標看到
註:按 Space 並拖曳可以很方便的移動視角
試試看這個函數:
其中 cel_ 開頭的東西是「地圖上的座標系統」,是螢幕座標的 1/8
而 sx, xy 是顯示在螢幕上的位置
那麼先不要管 layer,試著把地圖畫出來:
function _draw()
cls()
map(0, 0, 0, 0, 16, 16)
spr(1, pl.x, pl.y)
end因為我們在玩家碰到牆壁的時候做任何事
所以牆壁就只有圖像,沒有實質功能
那麼要怎麼樣有辦法讓玩家碰到牆會停住?
聽起來不難……
實作看看吧!
一個角色需要自己的碰撞箱
用來檢查有沒有撞到牆等等
我的角色大小是 5 x 7 像素
所以先以一個 5 x 7 的碰撞箱代表它
試試這個:
function _draw()
cls()
map(0, 0, 0, 0, 16, 16)
spr(1, pl.x, pl.y)
rect(pl.x, pl.y, pl.x + 4, pl.y + 6, 12)
end剛才的框框應該會像這樣:
往上移了一格……
因為這格 sprite 並不是在最左上角!
我留了一格之後做動畫比較方便
這時有兩種選擇:
1. 移動碰撞箱
2. 移動角色顯示的位置
但因為碰撞箱是用來判定的
所以它應該要是準確的
那……應該要移動角色顯示的位置
改一下 spr 的位置:
function _draw()
cls()
map(0, 0, 0, 0, 16, 16)
spr(1, pl.x, pl.y - 1)
rect(pl.x, pl.y, pl.x + 4, pl.y + 6, 12)
end然後保留圖像會有點花,所以我先把它註解掉了
function _draw()
cls()
map(0, 0, 0, 0, 16, 16)
-- spr(1, pl.x, pl.y - 1)
rect(pl.x, pl.y, pl.x + 4, pl.y + 6, 12)
end先想一下如何判斷有沒有撞到牆壁?
看四個角落就可以了!
註:這種做法是在「玩家碰撞箱大小 < 地圖每格大小」的前提下
否則有可能地圖會從兩個角中間卡進去
使用這個函數:
其中的 x, y 是地圖的 x, y
從螢幕座標轉換記得 ÷8
function _draw()
cls()
map(0, 0, 0, 0, 16, 16)
-- spr(1, pl.x, pl.y)
rect(pl.x, pl.y, pl.x + 4, pl.y + 6, 12)
if mget(pl.x / 8, pl.y / 8) == 64 then
pset(pl.x, pl.y, 8)
-- set screen pixel color
end
end左上角碰到牆壁就會變成紅色!
註:
在 _draw 函數中運算其實是很不好的做法
這裡只是做個測試,暫時寫在 _draw 以求方便
function rect_check(x, y, w, h, the_map)
return mget((x ) / 8, (y ) / 8) == the_map
or mget((x + w) / 8, (y ) / 8) == the_map
or mget((x ) / 8, (y + h) / 8) == the_map
or mget((x + w) / 8, (y + h) / 8) == the_map
end改成只要四個角其中一個有碰到
就把整個框框變紅色
寫成函數:
完整的程式碼有點塞不下了
之後應該都會放在下一頁,可以捲動看
-- 碰撞箱函數
function rect_check(x, y, w, h, the_map)
return mget((x ) / 8, (y ) / 8) == the_map
or mget((x + w) / 8, (y ) / 8) == the_map
or mget((x ) / 8, (y + h) / 8) == the_map
or mget((x + w) / 8, (y + h) / 8) == the_map
end
function _init()
pl = {
x = 59,
y = 59
}
-- 預設是藍色
box_color = 12
end
function _update()
-- 玩家移動
if btn(0) then
pl.x -= 1
end
if btn(1) then
pl.x += 1
end
if btn(2) then
pl.y -= 1
end
if btn(3) then
pl.y += 1
end
-- 如果碰到,換成紅色,否則是藍色
if rect_check(pl.x, pl.y, 4, 6, 64) then
box_color = 8
else
box_color = 12
end
end
function _draw()
cls()
map(0, 0, 0, 0, 16, 16)
-- spr(1, pl.x, pl.y - 1)
rect(pl.x, pl.y, pl.x + 4, pl.y + 6, box_color)
end如果玩家這 frame 移動後,碰到了牆壁
就把玩家推回去!
注意這邊新增了 xv、yv 變數(Velocity)
根據玩家輸入改變 xv、yv 的值
如果碰到牆就把速度減回去,就取消移動了
pl.x += pl.xv
pl.y += pl.yv
if rect_check(...) then
pl.x -= pl.xv
pl.y -= pl.yv
end輸入速度的方法:
-- 玩家移動 (x)
if btn(0) then
pl.xv = -1
elseif btn(1) then
pl.xv = 1
else
pl.xv = 0
end
-- 玩家移動 (y)
if btn(2) then
pl.yv = -1
elseif btn(3) then
pl.yv = 1
else
pl.yv = 0
end
剛才的方法如果左右同時按,會向左移動
可能會覺得「不要同時按就好啦」,但其實影響很多!
如何抵銷相反方向的速度?用減的!
如果按鈕的布林值轉換成數字
那麼 x 速度就是「右 - 左」,同理 y 速度就是「下 - 上」
-- 輸入速度
pl.xv = btn(1) - btn(0)
pl.yv = btn(3) - btn(2)出錯了,無法轉換布林值為數字!
這時我們需要一個轉換表
eval = {
[true] = 1,
[false] = 0
}eval[true]
-- returns 1
eval[false]
-- returns 0-- 右 - 左
pl.xv = eval[btn(1)] - eval[btn(0)]
-- 下 - 上
pl.yv = eval[btn(3)] - eval[btn(2)]
如果你按著右上
就會發現到了上方的牆邊就卡住了
但應該要繼續往右才對!
這是因為:
因為我們有水平和垂直的牆兩種
水平的牆不影響垂直移動
垂直的牆不影響水平移動
所以應該分開!
-- 更新玩家位置 (x)
pl.x += pl.xv
if rect_check(pl.x, pl.y, 4, 6, 64) then
pl.x -= pl.xv
end
-- 更新玩家位置 (y)
pl.y += pl.yv
if rect_check(pl.x, pl.y, 4, 6, 64) then
pl.y -= pl.yv
end假設我們要加速:
我寫得還可以……吧