精靈圖(Sprite)又叫做「拚合圖」,可將許多單一的小圖片合併成一張大圖,透過索引與座標來控制圖像的顯示。可減少檔案讀取時間,增加系統反應速度。精靈圖源於1974年街機興起,唯讀記憶體中的精靈圖被大量使用,增加遊戲畫面的可看性與顯示速度。
近年來網路技術興起,CSS 拼合圖(CSS Sprite)技術也借鏡早期精靈圖的核心,將需要分別顯示的多張圖片整合成單一大圖,利用CSS分別定位顯示各圖的技術,減少瀏覽器下載圖片的數量,提高網頁顯示速度。
玩學機除了物聯網的應用外,另外一個強項就是拿來開發復古小遊戲。玩學機團隊中,就有當年開發紅白機遊戲的資深工程師。團隊為了要引導孩子「做中學、玩中學」的玩學精神,特地在系統內建了一張 Sprite 圖。程式教學的教師與學生,可利用這些素材,做出生動有趣的小遊戲,讓學習者了解當年製作遊戲的許多技巧,更能理解電腦圖學教科書中的許多理論知識。
因為 Sprite 圖形已經燒錄到系統中,我們是無法直接修改它。為了要知道裡面有什麼元件,我們先寫一個小程式來抓出裡面的排列規則。
wb.blitpal()
while True:
for i in range(0, 80):
wb.blitbuf()
wb.blitimg(i*16, 0, 0, 128, 128)
if wb.getkey() == 1: # 我們運用一個技巧,按住右邊黃鍵
time.sleep(20) # 暫停20秒,可拿出手機拍攝記錄
wb.blit()
time.sleep(0.1)
透過擷取這些照片,我們可以推斷規則,這樣學員便可快速找出圖片的索引值,方便使用。幾個簡單的規則,整理如下:
其他還有許多圖案,有興趣的同學可以自行研究。
內建的 Sprite 圖檔為 128X640 大小的圖形,裡面的數字、符號與字元元素是 8x8 的像素圖檔,128 / 8 = 16,640 / 8 = 80,每一個像素圖檔有索引值可以呼叫指定的圖形。
wb.blitpal() # 啟動調色盤
for i in range(0,1024): # 啟動列印像素圖迴圈
wb.blitbuf() # 清除記憶空間
wb.blitimg(i, 8, 64, 8, 8) # 畫出圖型
if wb.getkey() == 64: # 按下右上方 menu 綠色按鍵暫停 10 秒
time.sleep(10)
wb.blit()
time.sleep(0.1)
# 印出藍白商標圖
for i in range(0,2): # 啟動列印像素圖迴圈
wb.blitbuf() # 清除記憶空間
# 印白色WiFiBoy.org
wb.blitimg(96, 8, 56, 64, 8)
# 印藍色WiFiBoy.org
wb.blitimg(104, 8, 64, 64, 8)
wb.blit()
time.sleep(0.1)
wb.cls()
wb.blitbuf()
wb.blitimg(464, 20, 4, 120, 16) # 地圖上面一排,索引第 464 個圖案,起點(20,4),長度120,原始圖案為16X16,取8x8 即為上半部
# 畫垂直邊界
for i in range(14):
wb.blitimg(424, 131, 12+i*8) # 右邊界圖像
wb.blitimg(421, 21, 12+i*8) # 左邊界圖像
wb.blitimg(480, 20, 124, 120, 8) # 地圖最下面一排。
# 畫地圖中間兩個牆壁
wb.blitimg(400, 40, 52, 8, 32); wb.blitimg(400, 112, 52, 8, 32)
# 畫地圖上方兩個牆壁
wb.blitimg(401, 40, 28, 32, 8); wb.blitimg(401, 88, 28, 32, 8)
# 畫地圖上方兩個牆壁
wb.blitimg(401, 40, 100, 32, 8); wb.blitimg(401, 88, 100, 32, 8)
# 畫地圖最中間的方塊,鬼出現的基地
wb.blitimg(405, 64, 52, 32, 32);
# 畫櫻桃
wb.blitimg(368, 72, 56, 16, 16)
# 畫地圖中間的豆子
for i in range(13):
for j in range(13):
if bean[i+j*13]==1: wb.blitimg(417, 28+i*8, 16+j*8)
elif bean[i+j*13]==2: wb.blitimg(418, 28+i*8, 16+j*8)
elif bean[i+j*13]==3: wb.blitimg(368, 72, 84, 16, 16)
wb.blit() # blit all to refresh display
感謝玩學運算科技公司提供底下的範例程式,讓我們可以了解如何在 短短兩百九十行內寫完一個這麼有趣的小精靈遊戲。
# WiFiBoy OK:ESP32 Pacman Clone Game (BLITENGINE)
# Jan 3, 2020, by Peter Gabriel under CC-BY-4.0 license
# Modifiy By Daniel Teng 2023.09.18
import machine, time
try:
if snd: snd.deinit()
except: pass
machine.Pin(17,3).value(1)
snd=machine.PWM(machine.Pin(25, 3))
snd.duty(0)
env1=[90,60,30,0,0,0,0,0]
env2=[90,60,30,20,15,10,5,0]
env3=[20,90,20,80,20,60,5,0]
env4=[90,20,90,20,10,20,10,0]
f=[round(110*2**(x/12)) for x in range(51)]
mel1=[22,13,0,0]
mel2=[30,32,34,36]
mel3=[30,32,38,0]
mel4=[16,22,28,0,26,0,28,0]
mel5=[39,0,51,0,46,0,43,0, 51,46,0,0,43,0,0,0,
40,0,52,0,47,0,44,0,52,0,47,0,44,0,0,0,
39,0,51,0,46,0,43,0, 51,46,0,0,43,0,0,0,
44,44,0,45,0,45,46,0,47,0,47,48,49,0,51,51,0]
mel6=[40,0,42,0,44,0,45,0,47,0,49,0,51,0,0]
mel7=[40,41,0,0,0,0,0,40,0,0]
def play(dt, mel, env, shift): # melody engine
for i in range(0, len(mel)):
if (mel[i]!=0):
snd.freq(f[mel[i]+shift])
ei=0
for j in range(0, 4):
if (ei<8):snd.duty(env[ei])
time.sleep(dt)
ei=ei+1
def soundit(f):
snd.freq(f)
snd.duty(90)
time.sleep(0.005)
snd.duty(0)
cposx = [0,48,96,0,24,48,72,96,0,24,48,72,96,0,48,96]
cposy = [0,0,0,24,24,24,24,24,72,72,72,72,72,96,96,96]
cturn = [1,6,2,5,6,8,6,7,5,8,6,8,7,3,8,4]
dirx=[0, 2, 0, -2] # up,right,down,left
diry=[-2, 0, 2, 0]
level = 1; life = 5
score = pwrcount = kbr = pani = pflp = 0
def reset_ghost():
global gx, gy, gdx, gdy, gtype, pwrcount
gx = [0, 96, 24, 72, 48, 0, 96, 96, 48]
gy = [0, 24, 48, 48, 24, 96, 96, 0, 0]
gdx = [1, 0, 0, 0, 1, 0, -1, -1, 0]
gdy = [0, -1, -1, 1, 0, -1, 0, 0, 1]
gtype = [0,0,0,0,0,0,0,0,0]
pwrcount=0
def reset_game():
global bean, bcount, px, py, dx, dy, pwrcount
bean = [2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2,
1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1,
2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2]
px = 48; py = 72
bcount = dx = dy = 0
pwrcount=0
reset_ghost()
play(0.012, mel5, env1, -24)
# 繪製場景
def draw_scene():
wb.blitbuf()
wb.blitimg(464, 20, 4, 120, 8)
for i in range(14): wb.blitimg(424, 131, 12+i*8); wb.blitimg(421, 21, 12+i*8)
wb.blitimg(480, 20, 124, 120, 8)
for i in range(13):
for j in range(13):
if bean[i+j*13]==1: wb.blitimg(417, 28+i*8, 16+j*8)
elif bean[i+j*13]==2: wb.blitimg(418, 28+i*8, 16+j*8)
elif bean[i+j*13]==3: wb.blitimg(368, 72, 84, 16, 16)
wb.blitimg(400, 40, 52, 8, 32); wb.blitimg(400, 112, 52, 8, 32)
wb.blitimg(401, 40, 28, 32, 8); wb.blitimg(401, 88, 28, 32, 8)
wb.blitimg(401, 40, 100, 32, 8); wb.blitimg(401, 88, 100, 32, 8)
wb.blitimg(405, 64, 52, 32, 32); wb.blitimg(368, 72, 56, 16, 16)
def draw_score():
wb.blitstr("SCORE "+str(score)+" Lv"+str(level),20,0)
wb.blitstr("x"+str(life),71,72);
wb.blit() # blit all to refresh display
def caught():
global life, level, score, px, py, dx, dy, bean
dx=dy=0
for i in range(8):
draw_scene()
wb.blitimg(304+i*2, px+24, py+12, 16, 16)
draw_score()
for j in range(8): soundit(90*(9-i)+j*50)
life -= 1
draw_scene()
if life==0: wb.blitstr("GAME OVER!", 40,24)
else: wb.blitstr("CAUGHT!!", 48,24)
wb.blitstr("PRESS A KEY", 37,96)
draw_score()
if life==0: life=5; score=0; level=1; reset_game()
else: px = 48; py = 72; dx = dy = 0
while wb.getkey()!=0: pass
while wb.getkey()==0: pass
reset_ghost()
if bean[123]==3: bean[123]=0 #clean 1up bonus
def eat_ghost(n):
global score, gx, gy, gdx, gdy, gtype
score+=100
gx[n]=gy[n]=48
gdx[n]=0; gdy[n]=-1
gtype[n]=0
play(0.005, mel6, env2, -24)
def ghost_turn2(n, a, b):
r = wb.rand(2)
if r==1: gdx[n]=dirx[a]; gdy[n]=diry[a]
else: gdx[n]=dirx[b]; gdy[n]=diry[b]
def ghost_turn3(n, a, b, c):
r = wb.rand(3)
if r==1: gdx[n]=dirx[a]; gdy[n]=diry[a]
elif r==2: gdx[n]=dirx[b]; gdy[n]=diry[b]
else: gdx[n]=dirx[c]; gdy[n]=diry[c]
def ghost_go():
global gx, gy, gdx, gdy
for i in range(level):
if gx[i]==px and gy[i]==py:
if gtype[i]==1: eat_ghost(i)
else: caught()
return
gx[i] += gdx[i]
gy[i] += gdy[i]
if gx[i]==px and gy[i]==py:
if gtype[i]==1: eat_ghost(i)
else: caught()
return
if (gx[i]%8!=0) and (gy[i]%8!=0): return
for j in range(16):
if (gx[i]==cposx[j] and gy[i]==cposy[j]):
if cturn[j]==1: ghost_turn2(i, 1, 2)
elif cturn[j]==2: ghost_turn2(i, 2, 3)
elif cturn[j]==4: ghost_turn2(i, 0, 3)
elif cturn[j]==3: ghost_turn2(i, 0, 1)
elif cturn[j]==5: ghost_turn3(i, 0, 1, 2)
elif cturn[j]==6: ghost_turn3(i, 1, 2, 3)
elif cturn[j]==7: ghost_turn3(i, 0, 2, 3)
elif cturn[j]==8: ghost_turn3(i, 0, 1, 3)
def check_input():
global px, py, dx, dy, bcount, score, level, pwrcount, life
px += dx
py += dy
if (px%8!=0) and (py%8!=0): return
if pwrcount>0: soundit(200)
pos = int(px/8)+int(py/8)*13
if bean[pos] == 1:
bean[pos]=0
score += 10
if pwrcount>0: play(0.001, mel3, env3, 0)
else: soundit(300)
bcount += 1
if bcount==80: # level done
dx=dy=0
draw_scene()
pac_go()
wb.blitstr("CLEAN!!", 56,40)
wb.blitstr("NEXT LEVEL..", 36,112)
draw_score()
level += 1
if level > 9: level=9
while wb.getkey()!=0: pass
while wb.getkey()==0: pass
reset_game()
return
elif bcount==60: #60-bean 1up
if bean[0]+bean[12]+bean[156]+bean[168]>5:
bean[123]=3
play(0.005, mel1, env1, 0)
elif bean[pos] == 2:
bean[pos]=0 # power bean
play(0.005, mel2, env1, 0)
pwrcount=100
for i in range(level): gtype[i]=1
elif bean[pos] == 3:
life += 1; bean[pos] = 0
if life>9: life=9
play(0.005, mel7, env2, -2)
for i in range(16):
if (px==cposx[i] and py==cposy[i]):
kb = wb.getkey()
if cturn[i]==1: #corner 0
if kb&4==4: dx=2; dy=0
elif kb&16==16: dx=0; dy=2
else: dx=0; dy=0
elif cturn[i]==2:
if kb&8==8: dx=-2; dy=0
elif kb&16==16: dx=0; dy=2
else: dx=0; dy=0
elif cturn[i]==4:
if kb&32==32: dx=0; dy=-2
elif kb&8==8: dx=-2; dy=0
else: dx=0; dy=0
elif cturn[i]==3:
if kb&32==32: dx=0; dy=-2
elif kb&4==4: dx=2; dy=0
else: dx=0; dy=0
elif cturn[i]==5:
if kb&32==32: dx=0; dy=-2
elif kb&16==16: dx=0; dy=2
elif kb&4==4: dx=2; dy=0
elif dy==0: dx=0
elif cturn[i]==6:
if kb&16==16: dx=0; dy=2
elif kb&8==8: dx=-2; dy=0
elif kb&4==4: dx=2; dy=0
elif dx==0: dy=0
elif cturn[i]==7:
if kb&32==32: dx=0; dy=-2
elif kb&16==16: dx=0; dy=2
elif kb&8==8: dx=-2; dy=0
elif dy==0: dx=0
elif cturn[i]==8:
if kb&32==32: dx=0; dy=-2
elif kb&8==8: dx=-2; dy=0
elif kb&4==4: dx=2; dy=0
elif dx==0: dy=0
return
kb = wb.getkey()
if kb&32==32 and dy==2: dy=-2
elif kb&16==16 and dy==-2: dy=2
elif kb&8==8 and dx==2: dx=-2
elif kb&4==4 and dx==-2: dx=2
def pac_go():
global pani, pflp, pwrcount
pani+=1
if pani%4==0: pflp = 1-pflp
if pwrcount>0:
pwrcount -= 1
if pwrcount==0:
for i in range(level): gtype[i]=0
#ghost
for i in range(level):
if gtype[i]==1: wb.blitimg(376+pflp*2, gx[i]+24, gy[i]+12, 16,16)
else: wb.blitimg(336+(i%4)*4+pflp*2, gx[i]+24, gy[i]+12, 16,16)
#pacman
if dx>0: wb.blitimg(272+pflp*2, px+24, py+12, 16, 16)
elif dx<0: wb.blitimg(280+pflp*2, px+24, py+12, 16, 16)
elif dy<0: wb.blitimg(284+pflp*2, px+24, py+12, 16, 16)
elif dy>0: wb.blitimg(276+pflp*2, px+24, py+12, 16, 16)
else: wb.blitimg(304, px+24, py+12, 16, 16)
wb.blitbuf()
wb.blitimg(272, 41, 40, 16, 16)
wb.blitimg(336, 61, 40, 16, 16)
wb.blitimg(340, 81, 40, 16, 16)
wb.blitimg(344, 101, 40, 16, 16)
wb.blitstr("PacBoy Demo 2023", 16, 70)
wb.blit()
reset_game()
while True:
draw_scene()
check_input()
ghost_go()
pac_go()
draw_score()
堅持到現在真是不簡單,明天我們將要介紹「45行寫出小蜜蜂遊戲」。