บทความตอนสุดท้ายของการทำเกมเตตริสแบบง่าย (Simple Tetris) ที่ใช้ MicroPython กับไมโครคอนโทรลเลอร์ esp32 ตามที่ได้เขียนถึงในตอนที่ 1 และ 2 จาก 2 บทความแรกนั้น ผู้อ่านได้เรียนรู้การออกแบบโครงสร้างข้อมูล การวาดวัตถุที่ตกลงมาทั้ง 7 ชนิด และการควบคุมวัตถุให้เคลื่อนที่ไปทางซ้าย ขวา และการหมุน ส่วนในบทความที่ 2 ได้ให้วัตถุตกลงมาจากด้านบนและเก็บสถานะตำแหน่งของวัตถุเอาไว้ และในบทความนี้เป็นการทำให้วัตถุที่ตกลงมานั้นซ้อนกันได้ พร้อมทั้งการเลื่อนซ้าย ขวา และการหมุนวัตถุจะตรวจสอบการชนกับวัตถุเก่าที่เคยตกลงมาก่อน พร้อมทั้งตรวจสอบว่าเมื่อวัตถุตกลงมาจนถึงด้านล่างแล้วนั้นมีแถวใดบ้างที่ไม่มีช่องว่าง ถ้าพบแถวที่ไม่มีช่องว่างจะทำการลบแถวนั้นออกไป และสุดท้ายได้เพิ่มส่วนของการตรวจสอบการสิ้นสุดเกมในกรณีที่ไม่มีที่จะให้วัตถุตกลงมาและเคลื่อนที่ได้อีกดังภาพที่ 1 เป็นอันสิ้นสุดกระบวนการสร้างเกมแบบง่าย ๆ ของเรา
การตรวจสอบการชน
การตรวจสอบการชนเป็นการตรวจสอบว่าค่า 1 ใน actors ตัวที่ actorNo ในมุม actorRotate นั้นมีตำแหน่งหรือช่องใดบ้างที่ไปซ้อนทับช่องของฉากหลังหรือ Field ที่เป็น 1 ถ้าพบจะทำการคืนค่ากลับเป็น True ดังโค้ดต่อไปนี้ โดยโค้ดนี้มีการควบรวมเงื่อนไขเพื่อให้ผู้อ่านได้เห็นแนวทางการเขียนโค้ดเพื่อตรวจสอบเงื่อนไขด้วยการใช้ in และตัวแปรลิสต์ของภาษาไพธอน คือ รวมเงื่อนไขของวัตถุประเภทที่ 1,2 และ 3 กับวัตถุประเภท 4 และ 5 เนื่องจากมีความกว้างและสูงเท่ากันในทุกการหมุนวัตถุ ทำให้ลดจำนวนการใช้ if
if actorNo == 0:
if actorRotate == 0:
for j in range(4):
if (actors[actorNo][actorRotate][0][j] & field[posY][posX+j]):
return True
else:
for i in range(4):
if (actors[actorNo][actorRotate][i][0] & field[posY+i][posX]):
return True
else:
if actorNo in [1,2,3]:
if actorRotate in [0,2]:
for i in range(2):
for j in range(3):
if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
return True
else:
for i in range(3):
for j in range(2):
if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
return True
elif actorNo in [4,5]:
if actorRotate == 0:
for i in range(2):
for j in range(3):
if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
return True
else:
for i in range(3):
for j in range(2):
if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
return True
else:
for i in range(2):
for j in range(2):
if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
return True
return False
การเลื่อนไปทางซ้าย
หลักการทำงานของการตรวจสอบการเลื่อนไปทางซ้าย คือ
- ตรวจสอบก่อนว่ายังไปไม่ถึงขอบด้านซ้าย
- ตรวจสอบเพิ่มเติมว่าไม่พบการชนกับทางซ้าย
- ถ้าชนจะไม่ลดค่า posX
- ถ้าไม่ชนจะลดค่า posX
- ตรวจสอบเพิ่มเติมว่าไม่พบการชนกับทางซ้าย
ตัวอย่างโค้ดสำหรับตอบสนองการกดปุ่มเลื่อนวัตถุไปทางซ้ายเป็นดังนี้ ซึ่งจะพบว่าใช้หลักทดลองย้ายก่อน ถ้าย้ายแล้วไม่ชนจะถือว่าสำเร็จ แต่ถ้าย้ายแล้วชนจะใช้ค่าเดิมตามขั้นตอนที่กล่าวมา
if keL.value() == 0:
if posX > 0:
posX -= 1
if isCollide():
posX += 1
else:
updated = True
การเลื่อนไปทางขวา
กรณีของการเลื่อนไปทางขวามีความคล้ายกันกับกรณีทางซ้าย คือ
- ตรวจสอบว่ายังไม่ชนสุดขอบของทางด้านขวา
- ตรวจสอบเพิ่มเติมว่าไม่มีการชนเมื่อขยับไปทางขวา
- ถ้าเกิดการชนจะไม่เพิ่มค่า posX
- ถ้าไม่เกิดการชนกับวัตถุทางขวาจะเพิ่มค่าของ posX
- ตรวจสอบเพิ่มเติมว่าไม่มีการชนเมื่อขยับไปทางขวา
การหมุน
การหมุนมีความแตกต่างกับการเคลื่อนที่ไปทางซ้ายหรือขวา คือ เป็นพิจารณาว่าเมื่อทำการหมุนแล้วเกิดการชนหรือไม่ถ้าไม่จะดำเนินการหมุน แต่ถ้าชนจะไม่เปลี่ยนแปลงค่าองศาการหมุน ดังนี้
- ถ้าการหมุนไม่เกิดปัญหาเรื่องการชนขอบด้านขวา และด้านล่าง
- เปลี่ยนค่า actorRotate
- ทดสอบการชน
- ถ้าชนให้เปลี่ยน actorRotate เป็นค่าเดิม
- ถ้าไม่ชนให้ใช้ค่าการหมุนค่าใหม่แทนค่าเดิม
การตกลงมา
การตกลงมาของวัตถุอาศัยการขนเป็นตัวระบุตำแหน่งล่างสุดที่วัตถุจะตกลงมาได้ โดยหลักการทำงานเป็นดังนี้
- ถ้าตกลงมาจนถึงแถวสุดท้ายให้จำตำแหน่งของวัตถุ
- ถ้าไม่ใช้ให้ตรวจสอบการชนกับวัตถุด้านล่าง
- ถ้าเกิดการชนให้จดจำตำแหน่ง ณ ปัจจุบันเอาไว้ใน field และเริ่มต้นการสุ่มวัตถุใหม่ให้ตกลงมา
- ถ้าไม่เกิดการชนให้ข้ามไป
การตรวจสอบการพ่ายแพ้
การตรวจสอบการพ่ายแพ้กระทำโดยใช้เงื่อนไขของการแพ้ไว้ว่า ถ้าวัตถุที่ตกลงไม่มีช่องให้เลื่อนลงอีกแล้วหมายถึงจบเกม ดังนั้น การเขียนโปรแกรมจึงไปปรับส่วนของการตกลงมา ถ้าสร้งวัตถุใหม่ที่จะตกลงมาแล้วตรวจสอบการชนพบว่าชนกันด้านล่างแปลว่าวัตถุขยับไม่ได้อีกแล้วจึงให้จบเกมด้วยการให้ตัวแปร gameOver เป็น True พร้อมทั้งยกเลิกการทำงานของตัวตั้งเวลา Tmr และ Render พร้อมทั้งแสดงข้อความ Game Over ดังภาพที่ 1 ที่หน้าจอ ดังโค้ดต่อไปนี้
table()
newItem()
if (isCollide()):
gameOver = True
Tmr.deinit()
Render.deinit()
return
การตัดแถวที่เต็มแล้วออกไป
หลักการทำงานของการตัดแถวที่เต็มแล้วออกไป เป็นดังนี้
1 row = แถวสุดท้าย
2 ถ้าพบข้อมูลแถวที่ row เป็น 1 ทั้งแถว
2.1 ถ้าเป็นแถวบนสุดให้ลบแถวปัจจุบันทิ้ง
2.2 ถ้าไม่ใช่ให้คัดลอกแถวด้านบนลงมา และเปลี่ยนแถวด้านบนเป้น 0 ทั้งแถว
3 ถ้ายังพบ ให้ row = row - 1
4 กลับไปทำ 2 ถ้า row > 0
สามารถเขียนเป็นโค้ดโปรแกรมได้ดังนี้
row = maxRow-1
while row > 0:
if ([1,1,1,1,1,1,1,1,1,1] == field[row]):
if (row == 0): # 2.1
field[row] = [0,0,0,0,0,0,0,0,0,0]
else:
for r in range(row, 1, -1):
field[r] = field[r-1]
field[0] = [0,0,0,0,0,0,0,0,0,0]
else:
row = row - 1
จากโค้ดด้านบนจะพบว่า ถ้าพบ [1,1,1,1,1,1,1,1,1,1] ซึ่งเป็นค่าสถานะของแถวใด ๆ ใน field และการเปลี่ยนค่าทั้งแถวกระทำโดยการส่ง [0,0,0,0,0,0,0,0,0,0] ไปเก็บใน field[แถว]
ตัวอย่างโปรแกรม
จากหลักการทั้งหมดทำให้ได้โค้ดของ Simple Tetris ออกมาเป็นดังนี้
#################################################################
# tetris Ep3
# JarutEx 2021-11-08
#################################################################
import gc
import os
import sys
import time
import machine as mc
from machine import Pin,SPI, Timer
import math
import st7735 as tft
import vga1_16x16 as font
import random
#################################################################
###### setting ##################################################
#################################################################
gc.enable()
gc.collect()
mc.freq(240000000)
spi = SPI(2, baudrate=26000000,
sck=Pin(14), mosi=Pin(12),
polarity=0, phase=0)
#### Key
keL = Pin(39, Pin.IN, Pin.PULL_UP)
keU = Pin(34, Pin.IN, Pin.PULL_UP)
keD = Pin(35, Pin.IN, Pin.PULL_UP)
keR = Pin(32, Pin.IN, Pin.PULL_UP)
swM1 = Pin(33, Pin.IN, Pin.PULL_UP)
swM2 = Pin(25, Pin.IN, Pin.PULL_UP)
swA = Pin(26, Pin.IN, Pin.PULL_UP)
swB = Pin(27, Pin.IN, Pin.PULL_UP)
spk = Pin(19,Pin.OUT)
spk.on()
time.sleep(0.1)
spk.off()
scr = tft.ST7735(spi, 128, 160, dc=Pin(15, Pin.OUT), reset=Pin(13,Pin.OUT), cs=Pin(2, Pin.OUT), rotation=3)
scr.initr()
Tmr = Timer(0)
field = []
actors = [
[ # แบบ1
[[1,1,1,1], # rotate=0
[0,0,0,0],
[0,0,0,0],
[0,0,0,0]],
[[1,0,0,0], # rotate=1
[1,0,0,0],
[1,0,0,0],
[1,0,0,0]]
],
[ # แบบ 2
[[1,1,1,0], # rotate=0
[1,0,0,0],
[0,0,0,0],
[0,0,0,0]],
[[1,1,0,0], # rotate=1
[0,1,0,0],
[0,1,0,0],
[0,0,0,0]],
[[0,0,1,0], # rotate=2
[1,1,1,0],
[0,0,0,0],
[0,0,0,0]],
[[1,0,0,0], # rotate=3
[1,0,0,0],
[1,1,0,0],
[0,0,0,0]]
],
[ # แบบ 3
[[1,0,0,0], # rotate=0
[1,1,1,0],
[0,0,0,0],
[0,0,0,0]],
[[1,1,0,0], # rotate=1
[1,0,0,0],
[1,0,0,0],
[0,0,0,0]],
[[1,1,1,0], # rotate=2
[0,0,1,0],
[0,0,0,0],
[0,0,0,0]],
[[0,1,0,0], # rotate=3
[0,1,0,0],
[1,1,0,0],
[0,0,0,0]]
],
[ # แบบ 4
[[0,1,0,0], # rotate=0
[1,1,1,0],
[0,0,0,0],
[0,0,0,0]],
[[1,0,0,0], # rotate=1
[1,1,0,0],
[1,0,0,0],
[0,0,0,0]],
[[1,1,1,0], # rotate=2
[0,1,0,0],
[0,0,0,0],
[0,0,0,0]],
[[0,1,0,0], # rotate=3
[1,1,0,0],
[0,1,0,0],
[0,0,0,0]],
],
[ # แบบ 5
[[0,1,1,0], # rotate=0
[1,1,0,0],
[0,0,0,0],
[0,0,0,0]],
[[1,0,0,0], # rotate=1
[1,1,0,0],
[0,1,0,0],
[0,0,0,0]]
],
[ # แบบ 6
[[1,1,0,0], # rotate=0
[0,1,1,0],
[0,0,0,0],
[0,0,0,0]],
[[0,1,0,0], # rotate=1
[1,1,0,0],
[1,0,0,0],
[0,0,0,0]]
],
[ # แบบ 7
[[1,1,0,0], # rotate=0
[1,1,0,0],
[0,0,0,0],
[0,0,0,0]]
]
]
actorColors = [
tft.color565(232,232,64),
tft.color565(232,64,64),
tft.color565(232,64,232),
tft.color565(64,64,232),
tft.color565(64,232,64),
tft.color565(64,232,232),
tft.color565(232,232,232)
]
actorRotate = 0
actorNo = random.randint(0,len(actors)-1)
maxCol = 10
maxRow = 16
posX = 0
posY = 0
blankColor = tft.color565(48, 48, 48)
filledColor = tft.color565(192,192,192)
updated = False
gameOver = False
#################################################################
###### sub modules ##############################################
#################################################################
def splash():
scr.fill(tft.color565(0x00,0x00,0x00))
scr.text(font,"JarutEx", 20, 20, tft.YELLOW, tft.BLACK)
scr.text(font,"JarutEx", 21, 20, tft.YELLOW, tft.BLACK)
scr.text(font,"(C)2021", 40,48, tft.CYAN, tft.BLACK)
time.sleep_ms(2000)
scr.fill(tft.BLACK)
def genTable():
global field
for i in range(maxRow):
row = []
for j in range(maxCol):
row.append(0)
field.append(row)
def table(rowStart=0, rowEnd=maxRow):
for i in range(rowStart, rowEnd,1):
for j in range(maxCol):
x = j * 8 + 1
y = i * 8 + 1
w = 6
h = 6
if (field[i][j] == 0):
scr.fill_rect(x,y,w,h, blankColor)
else:
scr.fill_rect(x,y,w,h, filledColor)
def draw():
actor = actors[actorNo][actorRotate]
for i in range(4):
for j in range(4):
if actor[i][j]:
x = (posX+j) * 8 + 1
y = (posY+i) * 8 + 1
w = 6
h = 6
scr.fill_rect(x,y,w,h,actorColors[actorNo])
def cbFalling(x):
global posY,posX,actorRotate, actorNo, updated, gameOver, Tmr, Render
posY += 1
lastRow = 14
if actorNo == 0:
if actorRotate == 0:
lastRow = 15
else:
lastRow = 12
elif actorNo == 1:
if actorRotate == 0:
lastRow = 14
elif actorRotate == 1:
lastRow = 13
elif actorRotate == 2:
lastRow = 14
else:
lastRow = 13
elif actorNo == 2:
if actorRotate == 0:
lastRow = 14
elif actorRotate == 1:
lastRow = 13
elif actorRotate == 2:
lastRow = 14
else:
lastRow = 13
elif actorNo == 3:
if actorRotate == 0:
lastRow = 14
elif actorRotate == 1:
lastRow = 13
elif actorRotate == 2:
lastRow = 14
else:
lastRow = 13
elif actorNo == 4:
if actorRotate == 0:
lastRow = 14
else:
lastRow = 13
elif actorNo == 5:
if actorRotate == 0:
lastRow = 14
else:
lastRow = 13
else:
lastRow = 14
if (posY > lastRow):
for i in range(4):
for j in range(4):
if actors[actorNo][actorRotate][i][j] == 1:
field[posY+i-1][posX+j] = 1
removeRow()
table()
newItem()
else:
if (isCollide()):
for i in range(4):
for j in range(4):
if actors[actorNo][actorRotate][i][j] == 1:
field[posY+i-1][posX+j] = 1
removeRow()
table()
newItem()
if (isCollide()):
gameOver = True
Tmr.deinit()
Render.deinit()
return
updated = True
def newItem():
global actorNo, posX, posY, actorRotate
actorNo = random.randint(0,len(actors)-1)
posX = 0
posY = 0
actorRotate = 0
def isCollide():
if actorNo == 0:
if actorRotate == 0:
for j in range(4):
if (actors[actorNo][actorRotate][0][j] & field[posY][posX+j]):
return True
else:
for i in range(4):
if (actors[actorNo][actorRotate][i][0] & field[posY+i][posX]):
return True
else:
if actorNo in [1,2,3]:
if actorRotate in [0,2]:
for i in range(2):
for j in range(3):
if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
return True
else:
for i in range(3):
for j in range(2):
if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
return True
elif actorNo in [4,5]:
if actorRotate == 0:
for i in range(2):
for j in range(3):
if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
return True
else:
for i in range(3):
for j in range(2):
if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
return True
else:
for i in range(2):
for j in range(2):
if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
return True
return False
def cbRender(x):
global posY,posX,actorRotate, actorNo, updated
if swA.value() == 0: # rotate
if (actorNo == 0):
if (actorRotate == 0):
if (posX > (maxCol-4))
pass
else:
actorRotate = 1
if (isCollide()):
actorRotate = 0
else:
updated = True
else:
if (posY > maxRow-4):
pass
else:
actorRotate = 0
if (isCollide()):
actorRotate = 1
else:
updated = True
elif (actorNo == 1):
if actorRotate == 0:
actorRotate = 1
if (isCollide()):
actorRotate = 0
else:
updated = True
elif actorRotate == 1:
if (posX < (maxCol - 2)):
actorRotate = 2
if (isCollide()):
actorRotate = 1
else:
updated = True
else:
actorRotate = 1
elif actorRotate == 2:
actorRotate = 3
updated = True
else:
if (posX < (maxCol - 2)):
actorRotate = 0
updated = True
else:
actorRotate = 3
elif (actorNo == 2):
if actorRotate == 0:
actorRotate = 1
updated = True
elif actorRotate == 1:
if (posX < (maxCol - 2)):
actorRotate = 2
updated = True
else:
actorRotate = 1
elif actorRotate == 2:
actorRotate = 3
updated = True
else:
if (posX < (maxCol - 2)):
actorRotate = 0
updated = True
else:
actorRotate = 3
elif (actorNo == 3):
if actorRotate == 0:
actorRotate = 1
updated = True
elif actorRotate == 1:
if (posX < (maxCol - 2)):
actorRotate = 2
updated = True
else:
actorRotate = 1
elif actorRotate == 2:
actorRotate = 3
updated = True
else:
if (posX < (maxCol - 2)):
actorRotate = 0
updated = True
else:
actorRotate = 3
elif (actorNo == 4):
if actorRotate == 0:
actorRotate = 1
updated = True
else:
if (posX < (maxCol - 2)):
actorRotate = 0
updated = True
else:
actorRotate = 1
elif (actorNo == 5):
if actorRotate == 0:
actorRotate = 1
updated = True
else:
if (posX < (maxCol - 2)):
actorRotate = 0
updated = True
else:
actorRotate = 1
if keL.value() == 0:
if posX > 0:
posX -= 1
if isCollide():
posX += 1
else:
updated = True
if keR.value() == 0:
if (actorNo == 0):
if (actorRotate == 0):
maxX = maxCol-4
else:
maxX = maxCol-1
if posX < maxX:
posX += 1
if (isCollide()):
posX -= 1
else:
updated = True
elif (actorNo == 1):
if (actorRotate == 0):
maxX = maxCol-3
elif (actorRotate == 1):
maxX = maxCol-2
elif (actorRotate == 2):
maxX = maxCol-3
else:
maxX = maxCol-2
if posX < maxX:
posX += 1
if (isCollide()):
posX -= 1
else:
updated = True
elif (actorNo == 2):
if (actorRotate == 0):
maxX = maxCol-3
elif (actorRotate == 1):
maxX = maxCol-2
elif (actorRotate == 2):
maxX = maxCol-3
else:
maxX = maxCol-2
if posX < maxX:
posX += 1
if (isCollide()):
posX -= 1
else:
updated = True
elif (actorNo == 3):
if (actorRotate == 0):
maxX = maxCol-3
elif (actorRotate == 1):
maxX = maxCol-2
elif (actorRotate == 2):
maxX = maxCol-3
else:
maxX = maxCol-2
if posX < maxX:
posX += 1
if (isCollide()):
posX -= 1
else:
updated = True
elif (actorNo == 4):
if (actorRotate == 0):
maxX = maxCol-3
else:
maxX = maxCol-2
if posX < maxX:
posX += 1
if (isCollide()):
posX -= 1
else:
updated = True
elif (actorNo == 5):
if (actorRotate == 0):
maxX = maxCol-3
else:
maxX = maxCol-2
if posX < maxX:
posX += 1
if (isCollide()):
posX -= 1
else:
updated = True
elif (actorNo == 6):
if posX < (maxCol-2):
posX += 1
if (isCollide()):
posX -= 1
else:
updated = True
if (updated):
startY = 0
endY = posY
if posY > 0:
startY = posY-1
if endY >= maxRow:
endY = maxRow
if actorNo == 0:
endY += 4
elif actorNo == 1:
endY += 3
elif actorNo == 2:
endY += 3
elif actorNo == 3:
endY += 3
elif actorNo == 4:
endY += 3
elif actorNo == 5:
endY += 3
else:
endY += 2
if endY >= maxRow:
endY = maxRow
table(startY,endY)
draw()
updated = False
def removeRow():
row = maxRow-1
while row > 0:
if ([1,1,1,1,1,1,1,1,1,1] == field[row]):
if (row == 0):
field[row] = [0,0,0,0,0,0,0,0,0,0]
else:
for r in range(row, 1, -1):
field[r] = field[r-1]
field[0] = [0,0,0,0,0,0,0,0,0,0]
else:
row = row - 1
#################################################################
###### main program #############################################
#################################################################
splash()
genTable()
table()
draw()
Render = Timer(1)
Tmr.init( period=1000, mode=Timer.PERIODIC, callback=cbFalling)
Render.init( period=100, mode=Timer.PERIODIC, callback=cbRender)
while swM1.value():
if gameOver:
scr.text(font," Game ", 40, 40, tft.YELLOW, tft.RED)
scr.text(font," Over ", 40, 56, tft.YELLOW, tft.RED)
break
scr.fill(0)
spi.deinit()
สรุป
จากบทความนี้จะพบว่าเกมเตตริสของเรานั้นสามารถย้ายตำแหน่งได้อย่างถูกต้อง หมุนได้อย่างถูกต้อง ตัดแถวที่เต็มแล้วออกไปได้อย่างถูกต้อง และมีการตรวจสอบการสิ้นสุดเกม แต่อย่างไรก็ดี สิ่งที่ขาดหายไปของเกม คือ ระบบเสียง การกดให้วัตถุตกลงมาโดยไม่ต้องรอ เอนิเมชันการตัดออก การให้คะแนน และด่านของเกม ซึ่งเป็นส่วนที่เติมเต็มให้เกมมีความสมบูรณ์ ท้าทาย และสร้างความสนุก สุดท้ายนี้หวังว่าตัวอย่างของบทความนี้คงมีประโยชน์และสร้างแนวคิดให้กับผู้ที่สนใจพัฒนาเกมแบบทำเองง่าย ๆ และสามารถทำได้ด้วยตนเอง ขอให้สนุกกับการเขียนโปรแกรมครับ
(C) 2020-2022, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-11-09, 2022-01-17