Snake Game in Python (Part: 2)
ဒါကတော့ Snake Game Tutorial လေးရဲ့ Part 2 ဖြစ်ပါတယ်။ Tutorial 1 မှာ Game window နဲ့ Control တွေ ရေးခဲ့ပါတယ်။ ဒီအပိုင်းမှာတော့ Collision Detection, Snake Movement, Scoring နဲ့ UI တွေ ဆက်ရေးမှာ ဖြစ်ပါတယ်။
What you'll learn from this project
- OOP ကို လက်တွေ့ အသုံးပြုပုံ
- Pygame အခြေခံ အသုံးပြုပုံ
- User input လက်ခံခြင်း
- Grid System
- Scoring System
- Basic Game Structure
Collision Detections
Collision စစ်တယ်ဆိုတာ ဂိမ်းထဲမှာ Sprite တစ်ခုနဲ့ တစ်ခုထိ၊ မထိ စစ်ပေးတာပါ။ ဒီဂိမ်းလေးထဲမှာဆိုရင် Snake နဲ့ နံရံတိုက်မိလား၊ ပန်းသီးကို ထိပြီလား သိရအောင် Collision စစ်ပေးရပါမယ်။
Random Apple
Collision မစစ်ခင်မှာ Apple class မှာ x, y ကို Random position ပြောင်းပေးဖို့အတွက် Method တစ်ခုရေးပါမယ်။ ဒါကို ရေးပြီးပြီဆိုရင် Apple.init() ထဲမှာ ခေါ်သုံးလို့ရပါတယ်။ Program အစမှာ Apple က random နေရာတွေမှာ ပေါ်နေမှာပါ။ Apple class ပုံစံအသစ်ကို အောက်မှာ ပြထားပါတယ်။ x, y parameter တွေကို ယူစရာမလိုတော့ပါဘူး။
class Game:
def __init__(self):
...
# Remove fixed x, y values
self.apple = Apple(self.screen)
class Apple:
def __init__(self, screen):
self.screen = screen
self.x = 0
self.y = 0
# Move to random x, y
self.move_random()
def move_random(self):
self.x = random.randrange(GRID)
self.y = random.randrange(GRID)
def draw(self):
# Nothing change
...
Eat That Apple!
Snake class ထဲမှာ snake အရှည်ကို တိုးပေးတဲ့ Method တစ်ခုရေးပါမယ်။ သူ့ရဲ့ လုပ်ဆောင်ချက်ကတော့ self.body ထဲမှာ နောက်ဆုံးတန်ဖိုးကို ကူးပြီး ထပ်ထည့်ပေးလိုက်တာပါ။ ဒါကို ဂိမ်းထဲမှာ ကြည့်လိုက်ရင် အပိုင်းတစ်ခု တိုးလာတဲ့ ပုံစံမြင်ရမှာပါ။
class Snake:
...
def eat(self):
self.body.append(self.body[-1].copy())
ဒါဆိုရင် Game class ထဲမှာ Collision စစ်ပြီး ဒီ Method နှစ်ခုကို သုံးလို့ ရပါပြီ။
class Game:
def check_collisions(self):
snake_head = self.snake.body[0]
if snake_head.x == self.apple.x and self.snake_head.y == self.apple.y:
self.snake.eat()
self.apple.move_random()
ပြီးရင် check_collisions() method ကို update ထဲမှာ ခေါ်ပြီး run လိုက်ပါ။ Snake နဲ့ apple ထိတိုင်း Apple ကနေရာပြောင်းသွားမှာ ဖြစ်ပါတယ်။ Snake length ကတော့ တစ်ခါတိုးလာပြီး နေရာမှာ တင်ကျန်နေခဲ့မှာပါ။ ဒါက ဘာကြောင့်လဲဆိုတော့ Snake.move ထဲမှာ ခေါင်းပိုင်းကို ပဲ နေရာပြောင်းထားပြီး ကျန်တဲ့ အပိုင်းတွေကို ဆက်ပြောင်းမထားလို့ ဖြစ်ပါတယ်။ ဒါကို Snake movement ရေးတဲ့ အခါမှာ ပြင်ပေးပါမယ်။
Checking Walls
အခုကတော့ Collision ရေးလက်စနဲ့ Border collision ရေးပါမယ်။ အရင်ဆုံး Game.restart() ထဲမှာ self.snake ကို ပြန်ခေါ်ပေးထားပါ။ Restart လုပ်တဲ့ အခါမှာ Snake ကို အစ ကနေပြန်စပေးတာပါ။ Border စစ်တဲ့ ပုံစံကတော့ အပေါ်က method နဲ့ ပုံစံတူပါတယ်။ ဒီမှာတော့ x, y သပ်သပ်စစ်ပေးရပါတယ်။ x, y တန်ဖိုးတွေက GRID အပြင်ရောက်သွားရင် True တန်ဖိုးထုတ်ပေးပါမယ်။
class Game:
...
def restart(self):
self.snake = Snake(self.screen)
def check_borders(self):
snake_head = self.snake.body[0]
if snake_head.x >= GRID or snake_head.x < 0:
return True
if snake_head.y >= GRID or snake_head.y < 0:
return True
return False
Game.check_collisions() ထဲမှာ အခုရေးထားတဲ့ Method နဲ့ စစ်ပြီး ဂိမ်းကို restart လုပ်ပေးလိုက်ပါမယ်။
class Game:
def check_collisions(self):
...
if self.check_borders():
self.restart()
ဒီအဆင့်ထိ အဆင်ပြေ၊ မပြေသိရအောင် run ကြည့်ပါ။ အကုန်အလုပ်လုပ်တယ်ဆိုရင် နောက်တစ်ဆင့် ဆက်သွားပါမယ်။
Snake Movement
အခုဆိုရင် Snake လေးက အရှည်တိုးလာပေမယ့် တိုးလာတဲ့ အပိုင်းတွေက လိုက်မရွေ့ဘဲ ကျန်နေခဲ့ပါတယ်။ ဒါကို Snake.move() method ထဲမှာ ပြင်ရပါမယ်။ move() ထဲမှာ self.body ရဲ့ နောက်ဆုံးပိုင်းကနေစပြီး ရှေ့အပိုင်းတစ်ခုစီကို ကူးပြီး ထည့်ပေးရပါမယ်။ ပြီးမှ ခေါင်းပိုင်းကို self.direction ပေါင်းထည့်ပေးပါမယ်။ ဒါဆိုရင် Snake က တစ်ဆက်တည်းရွေ့သွားမှာ ဖြစ်ပါတယ်။
class Snake:
...
def move(self):
for i in range(len(self.body)-1, 0, -1):
self.body[i] = self.body[i-1].copy()
self.body[0] += self.direction
...
move() method ရဲ့ အလုပ်လုပ်ပုံကို အောက်မှာ နမူနာ ပြထားပါတယ်။
Snake.body=[3,2,1,0] ဆိုရင် Coordinate တွေကို ပုံမှာ ပြတဲ့ အတိုင်းသိမ်းထားမှာပါ။ ဒီနေရာမှာ ကြည့်ရလွယ်အောင် X-coor တန်ဖိုးတွေနဲ့ပဲ ပြထားပါတယ်။ အစိမ်းရောင်အကွက်ကလေးက Index=0 ဖြစ်ပြီး ခေါင်းပိုင်းလို့ ယူဆထားပါ။ Snake.move() ကိုခေါ်လိုက်ပြီဆိုရင် နောက်ဆုံး Index ကနေစပြီး သူ့ရှေ့က တန်ဖိုးတွေကို ကူးထည့်သွားပါတယ်။ ပုံမှာဆိုရင် နောက်ဆုံး Index=3 က ရှေ့ကတန်ဖိုးကို ကူးလိုက်တဲ့အခါမှာ 0 ကနေ 1 ဖြစ်သွားပါတယ်။ ဒီလို For loop နဲ့ တစ်ခုချင်းစီ Index=1 ရောက်တဲ့ အထိ ကူးထည့်သွားပါတယ်။ ဒီ Loop ပြီးတဲ့အခါမှာ Snake.body=[3,3,2,1] ဆိုပြီး ပြောင်းသွားပြီး ရှေ့ဆုံးတန်ဖိုးနှစ်ခု တူနေမှာပါ။ ဒီတော့မှ Index=0 မှာရှိတဲ့ တန်ဖိုးကို Snake.direction နဲ့ ပေါင်းလိုက်ပါတယ်။ ဒါဆိုရင် ထိပ်ဆုံးတန်ဖိုးက ရှေ့တိုးသွားပြီး ပုံမှာကြည့်လိုက်ရင် Snake.body တစ်ခုလုံးက ရှေ့တစ်ကွက်ရွေ့သွားတဲ့ ပုံစံပေါ်လာမှာပါ။ ဒီလိုမျိုး Snake.move() တစ်ခါခေါ်တိုင်း တစ်ကွက်စီ ရွေ့သွားပြီး ဂိမ်းထဲမှာ Snake လေးသွားနေတဲ့ Effect လေးဖြစ်လာပါတယ်။ ဒီလိုပုံစံဖြစ်အောင် ရေးလို့ရတဲ့ တစ်ခြားနည်းတွေ အများကြီးရှိပါတယ်။ ကိုယ်တိုင်စမ်းရေးကြည့်ပါ။ ဒီနည်းကတော့ ရှင်းပြီး ရေးရလွယ်လို့ သုံးထားတာ ဖြစ်ပါတယ်။
Score & Highscore
ဒီအဆင့်ထိရောက်ရင် ဂိမ်းလေးက ပြီးသလောက်ဖြစ်နေပါပြီ။ ဂိမ်းပုံစံလေး ပြည့်စုံသွားအောင် Score ထည့်ပေးပါမယ်။ Game.init() ထဲမှာ self.score=0 ဆိုပြီး Attribute တစ်ခုထည့်လိုက်ပါ။ Highscore မှတ်ဖို့အတွက် self.hi_score=0 ဆိုပြီး ထပ်ထည့်ပါမယ်။ Snake နဲ့ Apple collision ဖြစ်တိုင်း Score တိုးပေးပါမယ်။ ဒါကို Game.check_collisions() ထဲမှာ ထည့်ပါမယ်။
class Game:
...
def check_collisions(self):
snake_head = self.snake.body[0]
if snake_head.x==self.apple.x and snake_head.y==self.apple.y:
...
self.score += 1
if self.score>self.hi_score:
self.hi_score=self.score
...
...
Displaying Text
Game class ထဲမှာ စာရေးဖို့အတွက် method တစ်ခုထည့်ပါမယ်။ Pygame ထဲမှာ စာရေးဖို့အတွက် အရင်ဆုံး Font သတ်မှတ်ပေးရပါတယ်။ သုံးလို့ရတဲ့ Font တွေသိချင်ရင် pygame.font.get_fonts() နဲ့ ကြည့်နိုင်ပါတယ်။ ပြီးရင် render လုပ်ပြီး ရေးမယ့်နေရာသတ်မှတ်ရပါတယ်။ ဒါတွေက Text တစ်ခုရေးတိုင်း လိုအပ်မယ့် ကုဒ်တွေဖြစ်ပါတယ်။ ဒါကြောင့် သူတို့ကို Function တစ်ခုနဲ့ ရေးထားလိုက်ရင် လိုအပ်တဲ့ အခါ ဒီ Function ခေါ်လိုက်ရုံပါပဲ။
class Game:
...
def write(self, text, size, x, y, color):
font = pg.font.SysFont('comicsansms', size)
message = font.render(text, True, color)
text_rect = message.get_rect()
text_rect.topleft = x, y
self.screen.blit(message, text_rect)
Game.draw() method ရဲ့ အောက်ဆုံးမှာ Score တွေကို write() နဲ့ ရေးပေးလိုက်ပါမယ်။ အရင်အပိုင်းမှာ Grid ဆွဲပြထားတဲ့ ကုဒ်တွေ ရှိသေးရင် ဖျက်ထားလိုက်ပါ။
class Game:
...
def draw(self):
...
self.write(f'Score: {self.score}', 50, 20, 5, 'white')
self.write(f'Best: {self.hi_score}', 50, 350, 5, 'white')
ပြီးရင် run လိုက်ပါ။ Snake Game လေးကို ကစားနိုင်ပါပြီ။ Color, Speed, Text တွေကို ကြိုက်သလို Customize လုပ်ကြည့်ပါ။ Snake speed ပြောင်းချင်ရင် Game class ထဲက snake_timer ကို အတိုး၊ အလျော့လုပ်ရပါမယ်။
Refining the game
ဂိမ်းလေးတစ်ခုလုံး ရေးပြီးသွားပေမယ့် ပြင်ဆင်စရာနည်းနည်း ရှိပါတယ်။ နံရံတွေနဲ့ တိုက်မိတဲ့အခါ Snake ကလေးက မူလပုံစံကနေ ပြန်စပေမယ့် Score က မပြောင်းပဲ ကျန်နေပါတယ်။ ဒါကိုတော့ Game.restart() method ထဲမှာ self.score=0 လို့ထည့်ပြီး ပြင်လိုက်ပါမယ်။ နောက်တစ်ခုကတော့ Snake အလျားရှည်လာတဲ့အခါမှာ Body ထဲကနေ ဖြတ်ပြီး သွားလို့ရတာကို တွေ့ရမှာပါ။ တစ်ကယ့် Snake Game ထဲမှာဆိုရင် ဒါကလည်း Collision တစ်ခုပါပဲ။ ဒါကို Game.check_collisions() ထဲမှာ ထည့်စစ်ပေးရပါမယ်။
class Game:
def check_collisions(self):
..
if snake_head in self.snake.body[1:]:
self.restart()
ဒီကုဒ်အလုပ်လုပ်ဖို့အတွက် Snake.init() ထဲမှာ self.body ကို အနည်းဆုံး တန်ဖိုးနှစ်ခုထည့်ထားပေးရပါမယ်။ self.direction=RIGHT ပေးထားရင် (x, y), (x-1, y), LEFT ဆိုရင် (x, y), (x+1, y) ဆိုပြီး တစ်ဆက်တည်းထည့်ထားလိုက်ပါ။ ဥပမာ- self.body=[Vector2(5, 5)] ဖြစ်ပြီး self.direction=RIGHT ဆိုရင် self.body=[Vector2(5,5), Vector2(4, 5)] ဆိုပြီး ထပ်ထည့်ရပါမယ်။ အဓိကကတော့ ဂိမ်းအစမှာ Snake ရဲ့ အရှည်ကို တစ်ခုထက် ပိုထားတဲ့ သဘောပါ။ Snake.body ထဲမှာ စစချင်း တန်ဖိုးတစ်ခုပဲ ရှိရင် အပေါ်က Collision စစ်တဲ့ အခါ Error တက်နိုင်ပါတယ်။ ဒီလို မဖြစ်အောင် ပိုကောင်းတဲ့ ရေးနည်းတွေ ရှိနိုင်ပေမယ့် စစချင်း Snake ရဲ့ body length တိုးပေးလိုက်တာက အလွယ်ဆုံးနဲ့ အမြန်ဆုံးနည်းလမ်းပါ။
Basic User-interface
ဂိမ်းတစ်ခုမှာ UI ပါမှ ပြည့်စုံပါတယ်။ ဝင်ဝင်ချင်း တန်းပြီး ကစား၊ ရှုံးတာနဲ့ ပြန်စဆိုရင် ဂိမ်းနဲ့ မတူတော့ပါဘူး။ မပါမဖြစ်ထည့်သင့်တာကတော့ Game over screen ပါ။
Gameover Screen
Game.init() ထဲမှာ self.gameover=False ဆိုပြီး Flag ထားလိုက်ပါ။ ပြီးရင် display_gameover() method ကိုရေးပါမယ်။
class Game:
...
def display_gameover(self):
self.screen.fill([200, 50, 40])
self.write('GAME OVER!', 50, 100, 30, 'white')
self.write(f'Score: {self.score}', 35, 180, 200, 'white')
self.write(f'Best: {self.hi_score}', 35, 180, 250, 'white')
self.write('press SPACE to play again', 20, 120, 350, 'white')
ပြီးရင် draw() နဲ့ update() method တွေကို ဒီလိုပြောင်းပေးရပါမယ်။
class Game:
...
def draw(self):
if self.gameover:
self.display_gameover()
return
...
def update(self):
pg.display.update()
self.clock.tick(FPS)
if self.gameover:
return
...
Game.restart() ခေါ်ထားတဲ့ နေရာတွေမှာ self.restart() ကို ဖျက်ပြီး self.gameover=True လို့ ပြောင်းရေးလိုက်ပါ။ ဒါဆိုရင် Collision ဖြစ်တဲ့ အခါမှာ Gameover screen ပေါ်လာမှာပါ။ Game.control() ထဲမှာ Space bar နှိပ်တဲ့အခါ restart လုပ်ပေးရပါမယ်။ ပြီးရင် တော့ restart() ထဲမှာ self.gameover=False လို့ ထည့်ပေးလိုက်ပါ။
class Game:
...
def restart(self):
...
self.gameover = False
def control(self):
...
if keys[pg.K_SPACE] and self.gameover:
self.restart()
...
ဒါကတော့ Gameover screen ကို နမူနာ ရေးပြထားတာပါ။ ဂိမ်းမစခင်မှာ ပြမယ့် Start screen / Main menu ကို ကိုယ်တိုင်ရေးကြည့်ပါ။ ရေးရမယ့် ပုံစံကတော့ ဒီအတိုင်းပါပဲ။ Project source code ထဲမှာ နမူနာကြည့်နိုင်ပါတယ်။
Additional Features
အခုရေးခဲ့တာတွေက Snake Game တစ်ခုမှာ ပါရမယ့် အခြေခံ Feature တွေပဲ ဖြစ်ပါတယ်။ ဒီအဆင့်ကနေပြီး နောက်ထပ်ကိုယ်ထည့်ချင်တဲ့ Feature တွေ ထပ်ထည့်နိုင်ပါတယ်။ ဥပမာ- Snake body ကို rect သုံးမယ့် အစား အဝိုင်းလေးတွေ ဆွဲနိုင်ပါတယ်။ Image တွေ ထည့်သုံးနိုင်ပါတယ်။ User ကြိုက်သလို ပြောင်းနိုင်မယ့် Theme တွေ ထည့်ပေးလို့ရပါတယ်။ Music, SFX တွေ ထည့်နိုင်ပါတယ်။ ဂိမ်းတစ်ခုက အသံမပါရင်လည်း မပြည့်စုံပါဘူး။ အသံတွေထည့်ဖို့အတွက် pygame.mixer ကို သုံးရပါမယ်။ ကိုယ်တိုင်လေ့လာပြီး ထည့်သုံးကြည့်ပါ။ ဂိမ်းရဲ့ Difficulty မြင့်အောင် ပန်းသီးစားဖို့ အချိန်သတ်မှတ်ထားတာ၊ အတားအဆီး ထည့်တာ၊ အရှိန်မြှင့်ပေးတာတွေ ထည့်ရေးနိုင်ပါတယ်။ ဒါတွေက ဂိမ်းလေးထဲထပ်ထည့်နိုင်တာတွေကို နမူနာအနေနဲ့ ပြောတာပါ။ ကိုယ်စိတ်ကူးကောင်းရင် ကောင်းသလို ဖန်တီးနိုင်ပါတယ်။ ရေးကြည့်လို့ အခက်အခဲရှိရင်လည်း Comment/Messenger မှာလာမေးနိုင်ပါတယ်။
Project Source Code နဲ့ Tutorial code တွေကို အောက်မှာ ပေးထားပါတယ်။ ဒီဂိမ်းလေးကို ရေးကြည့်ရင်းနဲ့ OOP concept တွေ၊ Game development အခြေခံတွေကို ပိုမိုနားလည်သဘောပေါက်သွားမယ်လို့ မျှော်လင့်ပါတယ်။
Github Repo => Classic Snake Game Project
Paste Bin => Tutorial Source Code
Happy Coding!