主題: 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. 輸出畫面
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 協議》,轉載必須註明作者和本文連結