002.04 Tkinter 星空大戰

Jason990420發表於2019-09-05

主題: 002.04 Tkinter星空大戰

建檔日期: 2019/09/05
更新日期: None

相關軟體資訊:

Win 10 Python 3.7.2 tkinter 8.6

說明:所有內容歡迎引用,只需註明來源及作者,本文內容如有錯誤或用詞不當,敬請指正.

為了更加熟練Tkinter的應用, 決定只用Tkinter來寫一個遊戲, 而不是用Pygame, 在過程中, 出了很多很大的問題, 最後留了一點已知的問題, 不想在花時間去找出來, 不過整個程式算跑的沒大問題.

1. 遊戲要求:

  • 星空中, 群星隨著宇宙飛船的前進, 而相對運動移動
  • 敵機群左右移動再下移, 碰到安全線, 玩家少一條命; 敵機群完全被消滅後提升一關, 速度再加快.
  • 玩家按鍵有左右鍵可以左右移動, 空格鍵可以發射子彈, 長按都可以持續動作, 按鍵Q可以隨時離開遊戲, 按鍵ESC可以暫停及恢復遊戲的進行.
  • 子彈碰到敵機, 兩者一起消滅, 得到一分.

  • 螢幕中顯現關數, 分數, 以及玩家所剩的生命數

2. 程式撰寫說明:

  • 星空移動: 轉換3D的星球座標(x,y,z)/半徑(r)為2D遊戲螢幕座標(x0,y0)/半徑(r0)

    factor = x - here
    x0 = y/factor
    y0 = z/factor
    r0 = r/factor
  • 宇宙飛船及敵機

    先建立背景可透明的圖檔(.png), 其alpha層透明的點值為0, 這樣的影像在背景上移動, 才能只顯示影像主體, 背景的星球移動才不會被遮住. 在視窗中就可以隨便擺放或移動影像, tkinter作法為如下:
    root = Tk()                                   #建立視窗
    canvas = Canvas(root,width=win.width,height=win.height) #在視窗上建立畫布 
    canvas.pack()                                 #擺上畫布
    im = Image.open('filename.png')               #開啟圖檔
    photo = ImageTk.PhotoImage(image=im)          #轉成PhotoImage物件
    Id = canvas.create_image(x, y, image=photo)   #放到畫布(x,y)位置
                                                  #Id則作以後面要作任何動作的依據
    canvas.coords(Id, x, y)                       #移動位置
  • 星球及子彈:

    基本上以畫圓來表示, 左上角座標(x0,y0), 右下角座標(x1,y1), 填上顏色color
    canvas.create_oval(x0, y0, x1, y1, fill=color) # 畫(橢)圓在畫布上
    canvas.coords(Id, x0, y0, x1, y1)              # 更動位置
    # 顯示及消失: 基本上以canvas.create_image及canvas.create_oval來顯示, 
    # 以canvas.delete(Id)來刪除.
  • 多執行緒控制:

    設定多少毫秒以後執行function, 以下方式就可以起動, 並定時執行. 必須注意多執行緒互相之間可能造成問題, 儘可能避免, 否則出錯非常難找到問題所在. 除了星球, 敵機及子彈的自行運動, 按鍵的重複觸發也可以使用, 比如系統本身自帶的第一次觸發會比較慢, 或觸發的間隔時間不符合要求, 也可以多執行緒的方式自行遮蔽處理.
    def function():
      pass    # do somthing here
      root.after(time, function)
    root.after(time, function)
  • 碰撞偵測

    tkinter可以找到在某一塊區域中所有影像的物件, 並以tag的方式來區隔所要的物件. 在敵機create_image及子彈create_oval中加入選項tags=’標籤’, 我們就可以用’標籤’找到子彈與敵機的碰撞, 進而消除子彈與敵機.
    overlap = canvas.find_overlapping(x0, y0, x1, y1)   # 找到物件列表
    overlap[index] in canvas.find_withtag(tag)          # 確認帶有該標籤tag的物件
  • 透明文字:

    在畫布中的文字是無法透明的, 我是以在新建透明影像中, 畫上文字的方式來達成要求.
    font = ImageFont.truetype("filename.ttf", font_size) # 建立字型及其大小
    im = Image.new('RGBA', (width, height))              # 建立含有alpha層的空影像
    im.putalpha(0)                                       # 設定alpha層的內容都為0(透明)
    draw = ImageDraw.Draw(im)                            #改為ImageDraw物件
    draw.text((x,y), text, fill=color, font=font)        # 畫上文字
    image = ImageTk.PhotoImage(im)                       # 轉成PhotoImage物件
    Id = canvas.create_image(x, y, image= image)         # 放到畫布(x,y)位置

3. 其他說明

  • IDE

    本來用的的Pyscripter, 但是在選寫程式中, 常會出錯, 而Tkinter中的root.mainloop()常會造成沒有響應無法停止, 唯一的方法只有停掉Pyscripter, 還有在Pyscripter中程式執行完, 還是會殘留在系統中, 尤其是Tkinter, 每次執行的結果都不一樣, 更糟的是無法執行, 因此換了Spyder, 問題就解決了, 不過其偵錯的功能就沒Pyscripter好用, 不過反正在Tkinter中, 其偵錯的功能也沒用.
  • 在程式撰寫過程中, 最常碰到的幾個問題

    程式庫: 首先, 基本上找不到最完整的說明書, 再來就是所有的引數沒有完全一致的定義或用法, 另外就是每個程式庫都是別人的一片天地, 要搞懂全部的內容真的很難. 因此在使用時, 定義, 用法, 功能常會出錯, 又受限於制定的內容而功能不齊, 不時的上網找答案.

    邏輯問題: 事件處理的先後, 方式, 常會造成難度或產生不同的問題, 比如使用class, fuction, method來撰寫一個一個的模組, 整個流程會更清楚, 找起問題也會容易. 像這個遊戲我大約寫了六個版本, 最後才定調, 其中自然是浪費了很多時間在重寫, 找問題除錯, 修改等等.

    已知問題: 有時候子彈發射會立刻停住, 但不影響遊戲的進行, 子彈沒少; 還有些list的index會超出範圍, 一樣不影響遊戲的進行.

4. 輸出畫面

002.04 Tkinter星空大戰

5. 程式說明

以下為完整的程式, 分為兩部份, 一個是遊戲常數, 避免常要宣告global, 所以放在另一個檔案, 便於尋找修改. 本來想用configparser中的config.ini的方式來作變數的宣告, 後來覺得又是另一個主題, 所以就沒有作下去. 不過內容仍然保留在主程式read_configuration()中; 其中還加了表示式功能, 改為a = ‘@b+c’, y就可以讀入處理; 另外就是使用exec(public)來作global宣告, exec(command)來執行python的每一行輸入. 程式中沒有加上任何說明, 因為作者累了, 哈哈哈哈 ! 請見量, 不過使用的變數名已經儘量用來說明該變數的意義.
# Main file StarWar.py
from tkinter import *
from PIL import Image, ImageTk, ImageDraw, ImageFont
import configparser
import random
from pre_init import *
import time

# default value

def read_configuration():
    config = configparser.RawConfigParser()
    config.read('d:\\configure.ini', encoding='utf-16')
    setting = config.sections()
    command = ''
    public = 'global '
    for group in setting:
        for option in config[group]:
            public += '%s,'%(option)
            value = config[group][option]
            if len(value)>2 and value[1]=='@':
                value = value[2:-1]
            command += '%s = %s\n' % (option, value)
    public = public[:-1]
    exec(public)
    exec(command)

# Window

def left_key():
    if key_pressed_left:
        ship.move_left()
        win.root.after(key_auto_time, left_key)

def right_key():
    if key_pressed_right:
        ship.move_right()
        win.root.after(key_auto_time, right_key)

def keydown(event):
    global key_pressed_left, key_pressed_right
    global key_pressed_space, key_pressed_ESC, stop_flag
    if event.keycode==81:
        game_over()
    elif event.keysym == 'Escape':
        if key_pressed_ESC:
            stop_flag = False
            threaded_enemy()
            threaded_bullet()
            key_pressed_ESC = False
        else:
            stop_flag = True
            key_pressed_ESC = True
    else:
        if event.keysym == 'space' and not key_pressed_space:
            bullet.count = 0
            bullet.new()
            key_pressed_space = True
        if event.keysym == 'Left' and not key_pressed_left:
            key_pressed_left = True
            left_key()
        if event.keysym == 'Right' and not key_pressed_right:
            key_pressed_right = True
            right_key()

def keyup(event):
    global key_pressed_left, key_pressed_right, key_pressed_space
    if event.keysym=='Left':
        key_pressed_left = False
    elif event.keysym=='Right':
        key_pressed_right = False
    elif event.keysym=='space':
        key_pressed_space = False

def convert(star_data):
    factor = star_data[0] - star_view_position
    if factor == 0:
        return [-1, -1, 1]
    x0 = int(star_data[1]/factor + win.half_width+1)
    y0 = int(star_data[2]/factor + win.half_height+1)
    r0 = int(star_data[3]/factor)
    return [x0, y0, r0]

class winObj():
    def __init__(self):
        self.root = Tk()
        self.root.bind(sequence='<KeyPress>', func=keydown)
        self.root.bind(sequence='<KeyRelease>', func=keyup)
        self.root.title(win_title)
        self.root.state('zoomed')
        self.root.update()
        self.root.resizable(width=False, height=False)
        self.width = self.root.winfo_width()
        self.half_width = self.width/2
        self.height = self.root.winfo_height()
        self.half_height = self.height/2

# Star

def threaded_star():
    star.move()
    win.root.after(star_flash_time, threaded_star)

class starObj():
    def __init__(self):
        self.range_x = star_x_range
        self.range_y = win.half_width*self.range_x
        self.range_z = self.range_y
    def show(self):
        self.all = []
        for i in range(star_total):
            while True:
                x = random.randint(self.range_x,
                    2*self.range_x)+star_view_position
                y = random.randint(-self.range_y, self.range_y)
                z = random.randint(-self.range_z, self.range_z)
                r = random.randint(star_min_dia, star_max_dia)
                x0, y0, r0 = convert((x, y, z, r))
                if (0 <= x0 < win.width and 0 <= y0 < win.height):
                    break
            ovalObj = canvas.create_oval(
                x0-r0, y0-r0, x0+r0, y0+r0, fill=star_color )
            self.all.append([x,y,z,r,ovalObj])
        threaded_star()
    def move(self):
        # 每一次更新所有的星球位置及大小
        global star_view_position
        star_view_position += star_move_step
        for i in range(star_total):
            while True:
                x0, y0, r0 = convert(self.all[i])
                #超出螢幕, 改成新星球, 座標及半徑都更新
                if (0 <= x0 < win.width and 0 <= y0 < win.height):
                    canvas.coords(self.all[i][4], x0-r0, y0-r0,
                        x0+r0, y0+r0)
                    break
                self.all[i] = [
                    random.randint(self.range_x, 2*self.range_x)+
                                   star_view_position,
                    random.randint(-self.range_y, self.range_y),
                    random.randint(-self.range_z, self.range_z),
                    random.randint(star_min_dia, star_max_dia),
                    self.all[i][4] ]
# Space Ship

class shipObj():
    def __init__(self):
        self.im = Image.open(ship_image_file)
        self.width = self.im.width
        self.height = self.im.height
        self.photo = ImageTk.PhotoImage(image=self.im)
        self.x = win.half_width
        self.y = win.height - int(self.height/2+1) - 5
    def show(self):
        self.Id = canvas.create_image(self.x, self.y, image=self.photo)
    def move_left(self):
        if self.width/2+ship_x_step <= ship.x:
            self.x -= ship_x_step
            canvas.coords(self.Id, self.x, self.y)
            canvas.update()
    def move_right(self):
        if self.x < win.width-ship_x_step-ship.width/2:
            self.x += ship_x_step
            canvas.coords(self.Id, self.x, self.y)
            canvas.update()

# Bullet

def threaded_bullet():
    global key_pressed_space
    if stop_flag:
        return
    if key_pressed_space:
        bullet.count += 1
        if bullet.count == bullet.count_limit:
            bullet.new()
            bullet.count = 0
    bullet.move()
    win.root.after(bullet_flash_time, threaded_bullet)

class bulletObj():
    def __init__(self):
        self.width = bullet_dia*2
        self.height = bullet_dia*2
        self.color = bullet_color
        self.count = 0
        self.count_limit = int(key_auto_time_space / bullet_flash_time + 1)
        self.all =[]
    def new(self):
        if len(self.all) >= bullet_total:
            return
        x1 = ship.x+bullet_start_x
        y1 = ship.y-bullet_start_y
        x2 = ship.x-bullet_start_x
        y2 = y1
        bullet_1 = canvas.create_oval(x1-bullet_dia, y1-bullet_dia, 
            x1+bullet_dia, y1+bullet_dia, fill=bullet_color, tags='object')
        bullet_2 = canvas.create_oval(x2-bullet_dia, y2-bullet_dia, 
            x2+bullet_dia, y2+bullet_dia, fill=bullet_color, tags='object')
        canvas.update()
        self.all.append([x1, y1, bullet_1])
        self.all.append([x2, y2, bullet_2])
    def renew(self):
        length = len(self.all)
        if length == 0:
            return
        for i in range(length):
            canvas.delete(self.all[i][2])
        canvas.update()
        self.all = []
    def collision(self, bull, index, x0, y0, x1, y1):
        overlap = canvas.find_overlapping(x0, y0, x1, y1)
        if len(overlap)==2 and (
            overlap[0] in canvas.find_withtag('object')):
            canvas.delete(overlap[0])
            canvas.delete(overlap[1])
            canvas.update()
            all = enemy.all[:]
            for i in range(len(enemy.all)):
                if enemy.all[i][0] == overlap[0]:
                    all.remove(enemy.all[i])
            enemy.all = all[:]
            bull.remove(self.all[index])
            if score.value < 10**score.digit-1:
                score.value += 1
                score.update()
    def move(self):
        length = len(self.all)
        boundary = bullet_step + bullet_dia
        if length == 0:
            return
        bull = [self.all[i] for i in range(len(self.all))]
        for i in range(length):
            print(i, length)
            if self.all[i][1] >= boundary:
                self.all[i][1] -= bullet_step
                x0 = self.all[i][0] - bullet_dia
                y0 = self.all[i][1] - bullet_dia
                x1 = self.all[i][0] + bullet_dia
                y1 = self.all[i][1] + bullet_dia
                canvas.coords(self.all[i][2], x0, y0, x1, y1)
                canvas.update()
                self.collision(bull, i, x0, y0, x1, y1)
            else:
                canvas.delete(self.all[i][2])
                bull.remove(self.all[i])
        self.all = [bull[i] for i in range(len(bull))]

# Enemy

def new_battle():
    global stop_flag
    level2.show()
    time.sleep(1)
    level2.hide()
    bullet.renew()
    enemy.renew()
    stop_flag = False
    threaded_bullet()
    threaded_enemy()

def kill_one():
    life.value -= 1
    life.update()
    return

def level_up():
    global enemy_step_x, enemy_step_y
    if level.value < 10**level_digit-1:
        level.value += 1
        level2.value += 1
        level.update()
        if enemy.x_step > 0:
            enemy.x_step += enemy_speed_plus
        else:
            enemy.x_step -= enemy_speed_plus
        enemy.y_step += 1

def threaded_enemy():
    global stop_flag
    if stop_flag:
        return
    if len(enemy.all) == 0:
        stop_flag = True
        level_up()
        new_battle()
        return
    else:
        enemy.move()
        if enemy.crash:
            kill_one()
            if life.value <= 0:
                stop_flag = True
                button.show()
                return
            else:
                stop_flag = True
                new_battle()
                return
    win.root.after(enemy_flash_time, threaded_enemy)

class enemyObj():
    def __init__(self):
        self.im       = Image.open(enemy_image_file)
        self.photo    = ImageTk.PhotoImage(image=self.im)
        self.half_w   = int(self.im.width/2)
        self.half_h   = int(self.im.height/2)
        self.x_step   = enemy_step_x
        self.y_step   = enemy_step_y
        self.right    = True
        self.down     = False
        self.col      = enemy_column_total
        self.row      = enemy_row_total
        self.total    = self.row * self.col
        self.left_bd  = enemy_step_x + enemy_x_border
        self.right_bd = win.width - self.left_bd
        self.limit    = line.line_position - self.half_h
        self.all      = []
    def renew(self):
        self.right = True
        self.down = False
        if len(self.all) != 0:
            for i in self.all:
                canvas.delete(i[0])
        self.all = []
        d = enemy_column_distance/2
        x0 = int(win.half_width - (self.half_w+d)*self.col + d)
        y0 = enemy_y_top_border+self.half_w
        for y in range(self.row):
            for x in range(self.col):
                x1 = x0 + x*(self.im.width + enemy_column_distance)
                y1 = y0 + y*(self.im.height + enemy_row_distance)
                Id = canvas.create_image(x1, y1, 
                    image=self.photo, tags='object')
                self.all.append([Id, x1, y1])
        canvas.update()
    def move(self):
        self.crash = False
        if len(self.all)==0:
            return
        if self.down:
            self.down = False
            self.right = not self.right
            for i in range(len(self.all)):
                self.all[i][2] =  self.all[i][2] + self.y_step
                if self.all[i][2] > self.limit:
                    self.crash = True
        else:
            if self.right:
                for i in range(len(self.all)):
                    self.all[i][1] = self.all[i][1] + self.x_step
                    if self.all[i][1] >= self.right_bd:
                        self.down = True
            else:
                for i in range(len(self.all)):
                    self.all[i][1] = self.all[i][1] - self.x_step
                    if self.all[i][1] <= self.left_bd:
                        self.down = True
        for i in range(len(self.all)):
            canvas.coords(self.all[i][0], self.all[i][1], self.all[i][2])
        canvas.update()

# Label

class labelObj():
    def __init__(self, text, digit, value):
        self.font   = ImageFont.truetype("C:\\Windows\\Fonts\\LUCON.TTF",
            label_font_size)
        self.text   = text
        self.digit  = digit
        self.value  = value
        self.width  = int((len(self.text+' ')+digit)*label_font_size*0.6+1)
        self.height = int(label_font_size*5/6+1)
        self.half_width  = int(self.width/2+1)
        self.half_height = int(self.height/2+1)
        if self.text == 'LEVEL':
            self.x = level_offset_x + self.half_width
            self.y = level_offset_y + self.half_height
        elif self.text == 'SCORE':
            self.x = win.half_width
            self.y = score_offset_y + self.half_height
        elif self.text == 'LIFE':
            self.x = win.width - life_offset_x - self.half_width
            self.y = life_offset_y + self.half_height
        elif self.text == 'LEVEL ':
            self.x = win.half_width
            self.y = win.half_height

    def show(self):
        self.prepare()
        self.Id = canvas.create_image(self.x, self.y, image=self.image)
        canvas.update()
    def hide(self):
        canvas.delete(self.Id)
        canvas.update()
    def update(self):
        self.prepare()
        canvas.itemconfigure(self.Id, image=self.image)
        canvas.update()
    def prepare(self):
        self.im = Image.new('RGBA', (self.width, self.height))
        self.im.putalpha(0)
        self.draw = ImageDraw.Draw(self.im)
        f = '{:0>'+str(self.digit)+'d}'
        self.draw.text(
            (0,0),
            self.text+' '+f.format(self.value),
            fill=label_color,
            font=self.font)
        self.image = ImageTk.PhotoImage(self.im)

# Button

def game_over():
    win.root.destroy()
    exit()

def game_start():
    global stop_flag
    button.hide()
    life.value = player_life
    level.value = player_level
    score.value = player_score
    life.update()
    score.update()
    level.update()
    stop_flag = False
    new_battle()

class buttonObj():
    def __init__(self):
        self.font = button_font+' '+str(button_font_size)+' '+button_font_style
        self.b1 = Button(
            win.root,
            width=(len(button1_text)+1)*2,
            height=2,
            font=self.font,
            text=button1_text,
            bg=button_bg,
            fg=button_fg,
            bd=2,
            command=game_start)
        self.b2 = Button(
            win.root,
            width=(len(button2_text)+1)*2,
            height=2,
            font=self.font,
            text=button2_text,
            bg=button_bg,
            fg=button_fg,
            bd=2,
            command=game_over)
        self.b1.pack()
        self.b2.pack()
    def show(self):
        self.w1 = canvas.create_window(
            win.half_width-(len(button1_text)+1)*button_font_size,
            win.half_height, window=self.b1)
        self.w2 = canvas.create_window(
            win.half_width+(len(button2_text)+1)*button_font_size,
            win.half_height, window=self.b2)
    def hide(self):
        canvas.delete(self.w1)
        canvas.delete(self.w2)

# Limit Line

class lineObj():
    def __init__(self):
        self.line_position = win.height-ship.height-20
    def show(self):
        self.Id = canvas.create_line(
            0,
            self.line_position,
            win.width,
            self.line_position,
            width=line_width,
            dash=line_dash,
            fill=line_color)

# Main

def show_all():
    star.show()
    level.show()
    score.show()
    life.show()
    ship.show()
    line.show()

def initial_object():
    global win, star, ship, canvas, line, enemy
    global level, score, life, bullet, button, level2
    win     = winObj()
    canvas = Canvas(win.root,bg='black',width=win.width,height=win.height)
    canvas.pack()
    star    = starObj()
    ship    = shipObj()
    line    = lineObj()
    enemy   = enemyObj()
    level   = labelObj(level_text, level_digit, player_level)
    score   = labelObj(score_text, score_digit, player_score)
    life    = labelObj(life_text , life_digit , player_life)
    level2  = labelObj('LEVEL ', level_digit, player_level)
    bullet  = bulletObj()
    button  = buttonObj()

def main():
    initial_object()
    show_all()
    button.show()
    # debug()
    win.root.mainloop()

def debug():
    try:
        print(enemy.all_backup[0][1])
    except:
        pass
    win.root.after(10, debug)

# Main scriptor start from here
if __name__ == '__main__':
    main()
# pre_init.py                                                
win_title = '星際大戰'
label_font_size = 60
message_font_size = 20
stop_flag = False
button_font = 'courier'
button_font_size = 20
button_font_style = 'bold'
button1_text = '遊戲開始'
button2_text = '遊戲結束'
button_bg = 'blue'
button_fg = 'white'
star_total = 200
star_move_step = 1
star_x_range = 100
star_view_position = 0
star_min_dia = 50
star_max_dia = 200
star_flash_time = 100
star_color = 'white'
ship_image_file = 'd:\\game\\space_ship.png'
ship_x_step = 20
enemy_image_file = 'd:\\game\\Enemy_small_new.png'
enemy_column_total = 10
enemy_column_distance = 50
enemy_row_total = 4
enemy_row_distance = 20
enemy_total = enemy_column_total  * enemy_row_total
enemy_step_x = 20
enemy_step_y = 20
enemy_x_border = 50
enemy_y_top_border = 120
enemy_flash_time = 100
enemy_speed_plus = 2
bullet_dia = 3
bullet_step = 20
bullet_start_x = 12
bullet_start_y = 28
bullet_total = 10
bullet = []
bullet_flash_time = 50
bullet_delete_all = False
bullet_color = '#ffff00'
player_level = 1
level_text = 'LEVEL'
level_digit = 2
level_offset_x = 30
level_offset_y = 10
player_score = 0
score_text = 'SCORE'
score_digit = 4
score_offset_x = 0
score_offset_y = 10
label_color = (128,128,0,255)
player_life = 3
life_text = 'LIFE'
life_digit = 2
life_offset_x = 30
life_offset_y = 10
line_width = 5
line_dash = (6,6)
line_color = 'green'
key_pressed_left = False
key_pressed_right = False
key_pressed_space = False
key_pressed_ESC = False
key_auto_time = 50
key_auto_time_space = 200
本作品採用《CC 協議》,轉載必須註明作者和本文連結

Jason Yang

相關文章