002.04 Tkinter 星空大战

主题: 002.04 Tkinter星空大战

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

相关软件信息:

Win 10 Python 3.7.2 tkinter 8.6

说明:所有内容欢迎引用,只需注明来源及作者,本文内容如有错误或用词不当,敬请指正.

002.04 Tkinter星空大战

为了更加熟练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
本帖由系统于 4年前 自动加精
讨论数量: 4
张雷

沙发我来坐,顶你!~

4年前 评论

pygame做游戏是不是更方便?

4年前 评论
Jason990420

目的不在游戏, 是在 tkinter ....

4年前 评论

膜拜,还以为tkinter只能写gui

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!