只在此山中,雲深不知處


聽首歌



© 2018 by Shawn Huang
Last Updated: 2018.5.27

Kivy


Installation

Kivy是由MIT研發的Python視窗程式套件。欲使用之,應先安裝Python。此處擬使用pycharm做為開發之IDE,首先自官網下載pycharm,請選擇Community版本。安裝之後開啟新專案,請選擇File>Settings,然後點選Project下之Python Interpreter,正確選擇上方之Python Interpreter,然後在下方的Package處,點選+,看是否有Kivy可以安裝。若無,請參考Kivy官網進行安裝。接著再回到pycharm應可以找到kivy,點選安裝即可。

Kivy Basic

首先在Pycharm建立一個Project,然後加上如下的Code:
from kivy.app import App
from kivy.uix.label import Label

class MyApp(App):
    def build(self):
        return Label(text="Kivy")

if __name__ == '__main__':
    MyApp().run()

Widgets
Widgets是視窗介面的元件,儲存在kivy.uix這個module內。其中傳統的基礎元件有Label, Button, CheckBox, Image, Slider, Progress Bar, Text Input, Toggle button, Switch, Video等。上例中我們使用Label來顯示文字,可以直接使用Label內建的方法來修改Label的顯示,例如:
from kivy.app import App
from kivy.uix.label import Label

class MyApp(App):
    def build(self):
        lb = Label()
        lb.font_size = '65' # 字體大小
        lb.text = "Kivy Label" # 文字內容
        lb.color = "red" # 文字顏色
        lb.text_size = (200, None) # 文字內容範圍,容納文字的框(寬,高),None表示高度不限
        return lb

if __name__ == '__main__':
    MyApp().run()
也可以使用如下的方法來控制Label內的文字,類似html(此處使用[])來改變文字顏色或其他屬性。Note: markup要等於True。
class MyApp(App):
    def build(self):
        return Label(text="[color=ff00ff]Kivy[/color]\n[color=00ffff]Label[/color]", markup = True)

Layout

當視窗中有多個元件,此時我們需要配置其對應位置及大小,這個工作由layout來完成。根據之前提到的kivy.uix module,可以看到layouts也是屬於一種widget,其中提供了Anchor Layout, Box Layout, Float Layout, Grid Layout, PageLayout, Relative Layout, Scatter Layout, Stack Layout等類型。此處先使用Grid Layout來練習。
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label

class MyApp(App):
    def build(self):
        lb1 = Label(text="Label 1")
        lb2 = Label(text="[color=ff00ff]Kivy[/color]\n[color=00ffff]Label[/color]", markup = True)
        lb3 = Label(text="Label 3")
        lb4 = Label(text="Label 4")
        lb5 = Label(text="Label 5")
        layout = GridLayout(cols=2)
        layout.add_widget(lb1)
        layout.add_widget(lb2)
        layout.add_widget(lb3)
        layout.add_widget(lb4)
        layout.add_widget(lb5)
        return layout

if __name__ == '__main__':
    MyApp().run()
也可以將layout分開為另一個class,寫法如下:
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label

class MyLayout(GridLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        lb1 = Label(text="Label 1")
        lb2 = Label(text="[color=ff00ff]Kivy[/color]\n[color=00ffff]Label[/color]", markup=True)
        lb3 = Label(text="Label 3")
        lb4 = Label(text="Label 4")
        lb5 = Label(text="Label 5")
        self.cols = 2
        self.add_widget(lb1)
        self.add_widget(lb2)
        self.add_widget(lb3)
        self.add_widget(lb4)
        self.add_widget(lb5)

class MyApp(App):
    def build(self):
        return MyLayout()

if __name__ == '__main__':
    MyApp().run()
分開寫的好處是讓我們更容易組織我們的工作。接下來在其內再加上一個button,將其至於最下方,為了將其與之前的label分開,我們將GridLayout分為兩個部分,Labels放在一個上層的GridLayout內,而新加入的button則與上層layout並列。如下:
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button

class MyLayout(GridLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.cols = 1

        self.topFrame = GridLayout()
        self.topFrame.cols = 2
        lb1 = Label(text="Label 1")
        lb2 = Label(text="Label 2")
        lb3 = Label(text="Label 3")
        lb4 = Label(text="Label 4")
        lb5 = Label(text="Label 5")
        self.topFrame.add_widget(lb1)
        self.topFrame.add_widget(lb2)
        self.topFrame.add_widget(lb3)
        self.topFrame.add_widget(lb4)
        self.topFrame.add_widget(lb5)

        self.add_widget(self.topFrame)
        bt = Button(text="Click")
        self.add_widget(bt)

class MyApp(App):
    def build(self):
        return MyLayout()

if __name__ == '__main__':
    MyApp().run()

bind()

有了按鈕,原則上點擊後我們期望他觸發事件,此時可以使用bind()方法來連結方法或函數。如下:
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button

class MyLayout(GridLayout):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.count = 0
        self.cols = 1

        self.topFrame = GridLayout()
        self.topFrame.cols = 2
        self.lb1 = Label(text="Label 1")
        self.lb2 = Label(text="Label 2")
        self.lb3 = Label(text="Label 3")
        self.lb4 = Label(text="Label 4")
        self.lb5 = Label(text="Label 5")
        self.lb6 = Label(text=f"{self.count}")
        self.topFrame.add_widget(self.lb1)
        self.topFrame.add_widget(self.lb2)
        self.topFrame.add_widget(self.lb3)
        self.topFrame.add_widget(self.lb4)
        self.topFrame.add_widget(self.lb5)
        self.topFrame.add_widget(self.lb6)

        self.add_widget(self.topFrame)
        self.bt = Button(text="Click")
        self.bt.bind(on_press=self.pressed)
        self.add_widget(self.bt)

    def pressed(self, instance):
        self.count += 1
        print(self.count, instance.text)
        self.lb6.text = f"{self.count}"

class MyApp(App):
    def build(self):
        return MyLayout()

if __name__ == '__main__':
    MyApp().run()

Kv Language

Kivy提供kv language可以讓我們將widget的架構分開撰寫,有點類似CSS。做法是同時開啟一個新檔案,使用跟class app同名的名稱(若是名稱為...App,需省略App),延伸檔名為kv。例如上例中為MyApp,所以我們須建立新檔案為my.kv(名稱使用小寫英文字母)。
首先將主程式(.py)的內容設定如下:
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
class MyLayout(Widget):
    pass

class MyApp(App):
    def build(self):
        return MyLayout()

if __name__ == '__main__':
    MyApp().run()
須注意要加上from kivy.uix.widget import Widget這個import,並將class MyLayout改為繼承Widget。接著在my.kv內加上以下內容:
<MyLayout>
    Label:
        text: "Label 1"
接著執行程式,可以看到視窗中包含一個Label。 接下來把my.kv內容增加如下:
<MyLayout>
    GridLayout:
        cols: 1
        Label:
            text: "Label 1"
        Button:
            text: "Click"
        GridLayout:
            cols: 2
            Label:
                text: "Label 2"
            Label:
                text: "Label 3"
            Label:
                text: "Label 4"
            Label:
                text: "Label 5"
            TextInput:
                multiline: False
執行程式後,可以看到其中有我們在my.kv內加入的元件(widget)以及所使用的layout。這個方式很顯然可以讓我們很容易地在.kv檔案內完成我們的版面配置,並可進行快速地調整修改或更新。
上例中的元件會擠在左下角,我們可以override size來使用全視窗,並將內容作稍許修改如下:
<MyLayout>
    GridLayout:
        cols: 1
        size: root.width, root.height
        Label:
            text: "Label 1"
        Button:
            text: "Click"
        GridLayout:
            rows: 2
            Label:
                text: "Label 2"
            Label:
                text: "Label 3"
            Label:
                text: "Label 4"
            Label:
                text: "Label 5"
        GridLayout:
            cols: 1
            TextInput:
                multiline: False
此時我們可以得到我們預期中的效果。因為我們將所有元件安排為樹狀結構,root表示整個視窗,root.width&root.height則表示整個視窗的寬高,你可以縮放視窗的大小看結果。甚至可以讓此二參數各-100,可以看到上方及右方縮減了,因為kivy的視窗原點為左下角,所以寬高由左下角往右及往上計算。

properties & bind

重新修改.py檔案,在class MyLayout其中加入兩個變數稱為name與age,如下:
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty

class MyLayout(Widget):
    name = ObjectProperty(None)
    age = ObjectProperty(None)

    def clickBtn(self):
        print(f"Name: {self.name.text}\nAge: {self.age.text}")
        self.name.text= ""
        self.age.text= ""

    def inputName(self):
        print("Input name......")

    def inputAge(self):
        print("Input age......")

class MyApp(App):
    def build(self):
        return MyLayout()

if __name__ == '__main__':
    MyApp().run()
因為要與.kv檔案內容呼應,無法像一般python的寫法直接定義變數,此處須導入property,可以自動更新自.kv處取得之變數值。使用時,必須宣告於class level,而不是宣告於任何的方法內(亦即不為method variable)。
除了變數之外,同時加入三個方法,其中self.name.text以及self.age.text指TextInput內的文字。
接著在my.kv內加入程式碼如下:
<MyLayout>
    name: name
    age: age

    GridLayout:
        cols: 1
        size: root.width, root.height
        GridLayout:
            cols: 2
            Label:
                text: "Name:"
            TextInput:
                id: name
                hint_text: "input your name......"
                multiline: False
                on_focus: root.inputName()
            Label:
                text: "Age:"
            TextInput:
                id: age
                hint_text: "input your age......"
                multiline: False
                on_focus: root.inputAge()
        Button:
            text: "Click"
            on_press: root.clickBtn()
如此可以得到我們需要的視窗配置。在my.kv內的<MyLayout>內,一樣定義name與age兩個變數用來呼應.py內的property。在對應的widget內,可以使用on_的事件(Event)觸發方法(e.g. on_press、on_text、on_mouseover、on_focus)來觸發對應方法。要注意的是當要呼叫MyLayout內的方法,需要使用root來作為前綴詞(root.methodName()),方可呼叫該方法(如前所述,root表示該視窗)。
BoxLayout & styles
現在嘗試使用kv language來實現boxlayout以及一些元件的styling方式。boxlayout是將元件做垂直或是水平方向的排列,預設方式為水平向,使用orientation參數。先看如下範例,首先.py檔案的程式碼如下:
from kivy.app import App
## from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty

class Root(Widget):

    def rootMethod(self):
        print("Root method")

class ALabel(Label):
    def __init__(self, **kwargs):
        super().__init__()
        self.color = "blue"

class AButton(Button):
    def __init__(self, **kwargs):
        super().__init__()
        self.color = "red"

    def clickbt(self):
        print("Button is clicked.")

class MyApp(App):
    def build(self):
        return Root()

if __name__ == '__main__':
    MyApp().run()
此處除了之前的class之外,額外設計了ALabel以及AButton兩個class,此二元件分別繼承自Label以及Button。接下來先看.kv檔案的程式碼,如下:
<Button>
    background_color: (0.5, 0.2, 0.3, 1)
    outline_color: "olive"
    outline_width: 3

<Root>
    BoxLayout:
        padding: 12
        spacing: 12
        size: root.width, root.height
        orientation: "vertical"

        BoxLayout:
            orientation: "horizontal"
            padding: 10
            spacing: 10
            Label:
                text: "Name:"
                ## Text properties
                bold: True
                italic: True
                color: "orange"
                ## background color
                background_color: (1,1,0,1)
                canvas.before:
                    Color:
                        rgba: self.background_color
                    Rectangle:
                        size: self.size
                        pos: self.pos
            TextInput:
                id: name
                hint_text: "input your name......"
                multiline: False

            ALabel:
                text: "Age:"
                font_size: 32

            TextInput:
                id: age
                hint_text: "input your age......"
                multiline: False

        Button:
            text: "Click"
            on_press: root.rootMethod()

        AButton:
            text: "Click Button"
            on_press: self.clickbt()
範例中共有六個元件(兩個Label、兩個TextInput、以及兩個Button),此處使用BoxLayout來布置,Label與TextInpu由一個BoxLayout布置(水平排列),再將此boxlayout與兩個Button放置於另一個boxlayout內(垂直排列)。改變style的寫法,可以約類分為以下三類:
  • 定義一個元件標籤,例如在最上方設計了一個<Button>物件的style,有點類似CSS的做法,此處定義的style將影響之後所有的Button。
  • 在各自的元件內加入想要使用的style,例如在第一個Label內的bold等相關參數。
  • 建立新的class,例如ALabel以及AButton(因為繼承自Label及Button,原則上就是Label&Button),在建構子內描繪style,之後在.kv檔內使用此元件即可(e.g. ALabel & AButton in .kv)。class內可以定義方法,在.kv中使用self.前綴來呼叫該方法即可,例如self.click()。

Touch

Kivy可以直接偵測滑鼠在其中的位置,事件名稱為on_touch_down、on_touch_up、以及on_touch_move,當牽扯到滑鼠位置時使用,使用下例說明,首先.py檔案內容如下:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.graphics import Color, Rectangle

class Root(Widget):
    pass

class ALabel(Label):
    def __init__(self, **kwargs):
        super().__init__()
        self.color = "black"
        with self.canvas:
            Color(0, .5, .5, 1)
            self.rect = Rectangle(pos=self.pos, size=self.size)

        self.bind(pos=self.updateRect, size=self.updateRect)

    def updateRect(self, instance, value):
        instance.rect.pos = instance.pos
        instance.rect.size = instance.size

    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            self.font_size = 32
            self.color = "red"
        return super().on_touch_down(touch)

    def on_touch_up(self, touch):
        if self.collide_point(*touch.pos):
            self.font_size = 16
            self.color = "black"
            self.outline_color = "olive"

        return super().on_touch_up(touch)

class AButton(Button):

    def __init__(self, **kwargs):
        super().__init__()
        self.color = "blue"
        self.background_color = (1, 1, 0, 1)

    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            self.background_color = (.5, .5, 0, .5)
            self.opacity = .3
            self.color = "red"

    def on_touch_up(self, touch):
        if self.collide_point(*touch.pos):
            self.background_color = (1, 1, 0, 1)
            self.opacity = 1
            self.color = "blue"

    def on_touch_move(self, touch):
        if self.collide_point(*touch.pos):
            print(f"position: {touch}")

class MyApp(App):
    def build(self):
        return Root()

if __name__ == '__main__':
    MyApp().run()
接下來看.kv的檔案內容:
<Root>
    BoxLayout:
        padding: 12
        spacing: 12
        size: root.width, root.height
        orientation: "vertical"
        ALabel:
            text: "Label 1"
        ALabel:
            text: "Label 2"
        AButton:
            text: "A"
        AButton:
            text: "B"
        AButton:
            text: "C"
這部分就沒甚麼好說明的了。
另一個寫法,如下:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.graphics import Color, Rectangle

class Root(Widget):
    pass

class ALabel(Label):
    def __init__(self, **kwargs):
        super().__init__()
        self.color = "pink"

    def style1(self):
        self.color = "green"
        self.font_size = 32

    def style2(self):
        self.color = "pink"
        self.font_size = 16

class AButton(Button):
    def __init__(self, **kwargs):
        super().__init__()
        self.color = "pink"

    def style1(self):
        self.color = "green"
        self.background_color = "olive"
        self.font_size = 32

    def style2(self):
        self.color = "pink"
        self.background_color = "orange"
        self.font_size = 16

class MyApp(App):
    def build(self):
        return Root()

if __name__ == '__main__':
    MyApp().run()
.kv檔案內容:
<Root>
    BoxLayout:
        padding: 12
        spacing: 12
        size: root.width, root.height
        orientation: "vertical"
        Label:
            text: "Label 1"
            on_touch_down: if self.collide_point(*args[1].pos): self.color = "red"
            on_touch_up: if self.collide_point(*args[1].pos): self.color = "blue"
        ALabel:
            text: "Label 2"
            on_touch_down: if self.collide_point(*args[1].pos): self.style1()
            on_touch_up: if self.collide_point(*args[1].pos): self.style2()
        Button:
            text: "A"
            on_touch_down: if self.collide_point(*args[1].pos): self.color = "red"
            on_touch_up: if self.collide_point(*args[1].pos): self.color = "blue"
        AButton:
            text: "B"
            on_touch_down: if self.collide_point(*args[1].pos): self.style1()
            on_touch_up: if self.collide_point(*args[1].pos): self.style2()
        Button:
            text: "C"

Screen Management

Kivy可以使用Screen Manager管理多個螢幕(Screen),預設值是管理一個Screen。我們可以直接在Screen內做配置,如下例:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.uix.screenmanager import Screen

class RootScreen(Screen):
    pass

class MyApp(App):
    def build(self):
        return RootScreen()

if __name__ == '__main__':
    MyApp().run()
要記得導入Screen: from kivy.uix.screenmanager import Screen
接著在.kv檔案內建立一個簡單的Button,如下:
<RootScreen>
    Button:
        text: "A Button"
        on_release: print(root.__class__)

Screen Manager

上例中只有一個Screen,若是要有多個Screen,則需要使用Screen Manager(也就是需要導入from kivy.uix.screenmanager import Screen, ScreenManager)。
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.uix.screenmanager import Screen, ScreenManager

class Manager(ScreenManager):
    pass

class MyApp(App):
    def build(self):
        return Manager()

if __name__ == '__main__':
    MyApp().run()
在.kv檔案中,設計兩個Screen,並分別在其中加入button,點擊前往不同的Screen。
<Manager>
    Screen:
        name: "Home"
        BoxLayout:
            orientation: "vertical"
            Label:
                text: "The Home Screen"
            Button:
                size: 100, 100
                size_hint: 1, None
                text: "To Next Screen"
                on_release: root.current = 'Next'

    Screen:
        name: "Next"
        BoxLayout:
            orientation: "vertical"
            Label:
                text: "The Second Screen"
            Button:
                size: 100, 100
                size_hint: 1, None
                text: "Back to Home"
                on_release: root.current = "Home"

Transitions

另一個Screen,可以有多種不同的花樣可做選擇,如下: 請自行在以下的程式碼選擇效果測試。
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.uix.screenmanager import Screen, ScreenManager, CardTransition, WipeTransition


class Manager(ScreenManager):
    pass

class Screen1(Screen):
    pass

class Screen2(Screen):
    pass

class Screen3(Screen):
    pass

class MyApp(App):
    def build(self):
        manager = Manager(transition=CardTransition())
        return manager

if __name__ == '__main__':
    MyApp().run()
在.kv檔案內的程式碼如下:
<Manager>:
    Screen1
    Screen2
    Screen3

<Screen1>:
    name: "Home"
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "The Home Screen"

        Button:
            size: 100, 100
            size_hint: 1, None
            text: "To Next Screen"
            on_release:
                root.manager.transition.direction = 'right'
                root.manager.transition.duration = 1
                root.manager.current = 'Second'

<Screen2>:
    name: "Second"
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "The Second Screen"
        GridLayout:
            size: 100, 100
            size_hint: 1, None
            cols: 2
            Button:
                text: "Back to Home"
                on_release:
                    root.manager.transition.direction = 'left'
                    root.manager.transition.duration = 1
                    root.manager.current = 'Home'
            Button:
                text: "To Next Screen"
                on_release:
                    root.manager.transition.direction = 'left'
                    root.manager.transition.duration = 1
                    root.manager.current = 'Third'
<Screen3>:
    name: "Third"
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "The Third Screen"

        GridLayout:
            size: 100, 100
            size_hint: 1, None
            cols: 2
            Button:
                text: "Back to Home"
                on_release:
                    root.manager.transition.direction = 'left'
                    root.manager.transition.duration = 1
                    root.manager.current = 'Home'
            Button:
                text: "To Pre Screen"
                on_release:
                    root.manager.transition.direction = 'left'
                    root.manager.transition.duration = 1
                    root.manager.current = 'Second'

Clock

clock物件可以讓我們排程函數,可能在未來呼叫一次或是在固定時間間隔持續呼叫,使用之前需先呼叫from kivy.clock import Clock,用法如下: 設計一個簡單的時鐘例子:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.clock import Clock
from datetime import datetime as dtime

class clock(Label):
    def update(self, *args):
        self.text = dtime.now().strftime("%Y-%B-%d %A %I:%M:%S %p")

class MyApp(App):
    def build(self):
        root = clock()
        Clock.schedule_interval(root.update, 1)
        return root

if __name__ == '__main__':
    MyApp().run()
Another Clock
此處介紹另一個寫法,以及加上cancel()跟schedule_once()等方法。在.py檔案中加入以下程式碼:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.clock import Clock
from datetime import datetime as dtime

class MyApp(App):
    def on_start(self):
        self.clock = Clock.schedule_interval(self.update_label, 1)
        Clock.schedule_once(self.stop_interval, 10)

    def stop_interval(self, *args):
        self.clock.cancel()

    def update_label(self, *args):
        self.root.ids.time.text = dtime.now().strftime("%Y-%B-%d %A %I:%M:%S %p")

if __name__ == '__main__':
    MyApp().run()
在.kv檔案中加入元件如下:
GridLayout:
    cols: 1
    Label:
        id: time
        text: "0"

Graphics

Graphics模組可讓我們繪製幾何圖形,使用前須匯入kivy.graphics模組,舉例如下:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import *

class Paint(Widget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        with self.canvas:
            Color(1,0,1,1, mode="rgba")
            self.rect = Rectangle(pos=(0,0), size=(20, 20))

    def on_touch_move(self, touch):
        x = touch.pos[0]-self.rect.size[0]/2
        y = touch.pos[1]-self.rect.size[1]/2
        self.rect.pos = (x,y)

class MyApp(App):
    def build(self):
        return Paint()

if __name__ == '__main__':
    MyApp().run()
再看另一個例子:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import *
from kivy.core.window import Window
import random as rd

class Paint(Widget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.size = Window.size ## Set the canvas size equal to the window size
        self.nodes = []
        self.draw_nodes()

    def draw_nodes(self):
        with self.canvas:
            Color(rd.random(), rd.random(), rd.random(), rd.random(), mode="rgba")
            for i in range(50):
                node = Rectangle(pos=(rd.random()*self.width, rd.random()*self.height), size=(5, 5))
                self.nodes.append(node)

    def on_touch_down(self, touch):
        with self.canvas:
            for node in self.nodes:
                Line(points=(touch.pos, node.pos))

    def on_touch_up(self, touch):
        self.canvas.clear()
        self.draw_nodes()


class MyApp(App):
    def build(self):
        return Paint()

if __name__ == '__main__':
    MyApp().run()

上傳到Mobile

請參考官網教學

Pygame

basic

Pygame是用來製作遊戲的Python模組,首先先安裝Pygame,在PyCharm的File > Settings之下按+,輸入pygame搜尋然後點擊安裝即可。開啟新Project後,輸入以下程式碼:
import sys
import pygame
from pygame.locals import QUIT

pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode((800, 600)) # 建立window並設定大小
pygame.display.set_caption("Hello") # 設定視窗標題
win.fill((0, 255, 255)) # 清除畫面並填滿背景顏色

## 關於文字
font = pygame.font.SysFont(None, 60) # 設定字型
surface = font.render("Hello, World!", True, (0,0,0)) # 回傳surface
win.blit(surface, (10, 10)) # blit用來把其他元素渲染到另一個surface

pygame.display.update() # 更新畫面,須有此行否則無內容

while True: # 主程式迴圈
    for event in pygame.event.get(): #事件的loop
        if event.type == QUIT:
            pygame.quit() # 退出pygame
            sys.exit() # 退出系統
貼圖
跟上例類似的做法,設計一個背景圖,再加上一個隨著游標移動的小圖,程式碼如下:
import sys
import pygame
from pygame.locals import *

background = 'your directory/background.png' # 背景圖路徑(最好跟視窗同大小,800x600)
sheep = 'your directory/sheep.png' # 游標圖路徑(這個圖可以先修改小一點)

pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode((800, 600)) # 建立window並設定大小
pygame.display.set_caption("Hello") # 設定視窗標題

## 關於圖片
bg = pygame.image.load(background).convert() # 讀取背景圖
sp = pygame.image.load(sheep).convert_alpha() # 讀取游標圖

while True: # 主程式迴圈
    for event in pygame.event.get(): #事件的loop
        if event.type == QUIT:
            pygame.quit() # 退出pygame
            sys.exit() # 退出系統

        win.blit(bg, (0, 0))  # blit用來把被競圖渲染到視窗

        x, y = pygame.mouse.get_pos() # 取得游標座標
        x -= sp.get_width()/2
        y -= sp.get_height()/2
        win.blit(sp, (x,y)) # 渲染至游標位置

        pygame.display.update()  # 更新畫面,須有此行否則無內容
  • 使用pygame.image.load()來讀取圖片,取得的圖片呼叫covert()可以將圖片轉換成跟display相同格式,如此可以較為快速地顯示因為兩者有相同的depth(number of colors),呼叫covert_alpha()是因為多了alpha information(translucent,不過結果好像沒有半透明,應該原圖就要設計是半透明)。結果顯示如下:
  • 將update()與blit()放置一起,讀取後顯示。

Event

在前例中,我們只處理了一個事件(Event)QUIT,此事件讓我們可以關閉視窗。除了QUIT之外,pygame還可以產生其他的事件。在前例中,我們呼叫pygame.event.get()來萃取所有的事件然後將其自佇列(Queue)中移除。以下條列一些我們可能用到的標準事件(Standard events)。see https://www.pygame.org/docs/ref/event.html for more details. 首先寫一個簡單的程式來顯示所有產生的事件。如下:
import sys
import pygame
from pygame.locals import *

pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode((800, 600), 0) # 建立window並設定大小
pygame.display.set_caption("Hello") # 設定視窗標題

while True: # 主程式迴圈
    for event in pygame.event.get(): #事件的loop
        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            sys.exit()  # 退出系統
        print(event)
        pygame.display.update()  # 更新畫面,須有此行否則無內容
跟之前的程式碼差不多,只是印出event,移動或點擊滑鼠或按下keyboard,我們可以在console看到多個事件顯示。

Mouse Motion&Button Events

滑鼠的事件主要是移動或是按下按鍵(buttons),使用一個tuple來對應三個按鍵,所以buttons[0]、buttons[1]、buttons[2]分別表示左鍵、中鍵(滾輪)、以及右鍵。當button被按下,其值為1,否則為0。另一個值是滑鼠位置(pos),也是使用tuple儲存滑鼠位置(x,y)。此外還有rel屬性,也是使用tuple來儲存滑鼠自上次到此次所移動的距離。
import sys
import pygame
from pygame.locals import *

pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode((800, 600), 0) # 建立window並設定大小
pygame.display.set_caption("Hello") # 設定視窗標題

while True: # 主程式迴圈
    for event in pygame.event.get(): #事件的loop
        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            sys.exit()  # 退出系統
        if event.type == MOUSEMOTION: ## 試著按著滑鼠按鍵移動
            print("distance moved:", event.rel, "pos:", event.pos, "buttons:", event.buttons)
        if event.type == MOUSEBUTTONDOWN:
            print("button:", event.button, "pos:", event.pos)
        pygame.display.update()  # 更新畫面,須有此行否則無內容
在測試時可以一次顯示一種事件,以免顯示太多顯得混亂。

Keyboard Events

使用KEYDOWN表示按鍵被按下(press),KEYUP表示放開(release)按鍵。試試看以下的例子:
import sys
import pygame
from pygame.locals import *

sp = 'sheep.png'  # 置於與程式相同的資料夾
bg = 'background.png'  # 置於與程式相同的資料夾
pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode((800, 600), 0)  # 建立window並設定大小
pygame.display.set_caption("Moving Sheep")  # 設定視窗標題

sheep = pygame.image.load(sp).convert()
background = pygame.image.load(bg).convert()
x, y = 0, 0

while True:  # 主程式迴圈
    for event in pygame.event.get():  # 事件的loop
        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            sys.exit()  # 退出系統
        if event.type == KEYDOWN:
            if event.key == K_LEFT:  # 向左鍵
                x -= 2
            elif event.key == K_RIGHT:  # 向右鍵
                x += 2
            elif event.key == K_UP:  # 向上鍵
                y -= 2
            elif event.key == K_DOWN:  # 向下鍵
                y += 2

        win.blit(background, (0, 0))
        win.blit(sheep, (x, y))
        pygame.display.update()  # 更新畫面,須有此行否則無內容
我們可以按方向鍵來移動sheep。至於要設計哪些按鍵,請參考https://www.pygame.org/docs/ref/key.html
Keep Moving
上例中,當按下一次按鍵,圖片才會移動一下,要讓圖片一直移動,必須要持續地按按鍵,我們可以使用如下的設計,當我們按住按鍵時,圖片便可持續的移動。
import sys
import pygame
from pygame.locals import *

sp = 'sheep.png'  # 置於與程式相同的資料夾
bg = 'background.png'  # 置於與程式相同的資料夾
pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode((800, 600), 0)  # 建立window並設定大小
pygame.display.set_caption("Moving Sheep")  # 設定視窗標題

sheep = pygame.image.load(sp).convert()
background = pygame.image.load(bg).convert()
x, y = 0, 0
move_right = False
move_left = False
move_up = False
move_down = False
while True:  # 主程式迴圈
    for event in pygame.event.get():  # 事件的loop
        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            sys.exit()  # 退出系統
        if event.type == KEYDOWN:
            if event.key == K_LEFT:  # 向左鍵
                move_left = True
            elif event.key == K_RIGHT:  # 向右鍵
                move_right = True
            elif event.key == K_UP:  # 向上鍵
                move_up = True
            elif event.key == K_DOWN:  # 向下鍵
                move_down = True
        elif event.type == KEYUP:
            if event.key == K_LEFT:  # 向左鍵
                move_left = False
            elif event.key == K_RIGHT:  # 向右鍵
                move_right = False
            elif event.key == K_UP:  # 向上鍵
                move_up = False
            elif event.key == K_DOWN:  # 向下鍵
                move_down = False
    if move_right:
        x += 1
    if move_left:
        x -= 1
    if move_up:
        y -= 1
    if move_down:
        y += 1
    win.blit(background, (0, 0))
    win.blit(sheep, (x, y))
    pygame.display.update()  # 更新畫面,須有此行否則無內容
另一個簡單的寫法:
import sys
import pygame
from pygame.locals import *

sp = 'sheep.png'  # 置於與程式相同的資料夾
bg = 'background.png'  # 置於與程式相同的資料夾
pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode((800, 600), 0)  # 建立window並設定大小
pygame.display.set_caption("Moving Sheep")  # 設定視窗標題

sheep = pygame.image.load(sp).convert()
background = pygame.image.load(bg).convert()
x, y = 0, 0

while True:  # 主程式迴圈
    for event in pygame.event.get():  # 事件的loop
        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            sys.exit()  # 退出系統
    keys = pygame.key.get_pressed()  # 偵測被按下的按鍵
    if keys[pygame.K_RIGHT]:
        x += 1
    if keys[pygame.K_LEFT]:
        x -= 1
    if keys[pygame.K_UP]:
        y -= 1
    if keys[pygame.K_DOWN]:
        y += 1

    win.blit(background, (0, 0))
    win.blit(sheep, (x, y))
    pygame.display.update()  # 更新畫面,須有此行否則無內容
按鍵控制全螢幕
設計一個程式,按下某鍵變成全螢幕,放開後變回原大小。(也可以設計一鍵全螢幕,另一鍵回原大小)
import sys
import pygame
from pygame.locals import *

sp = 'sheep.png'  # 置於與程式相同的資料夾
bg = 'background.png'  # 置於與程式相同的資料夾
pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode((640, 480), 0, 32)  # 建立window並設定大小
background = pygame.image.load(bg).convert()

while True:  # 主程式迴圈
    for event in pygame.event.get():  # 事件的loop
        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            sys.exit()  # 退出系統
        if event.type == KEYDOWN:
            if event.key == K_f:  # 按下f鍵
                win = pygame.display.set_mode((640, 480), FULLSCREEN, 32)
        elif event.type == KEYUP:
            if event.key ==K_f: # 放開f鍵
                win = pygame.display.set_mode((640, 480), 0, 32)

        win.blit(background, (0, 0))
        pygame.display.update()  # 更新畫面,須有此行否則無內容

Other Events

並非所有events都需要被handle,例如在之前的例子中,使用pygame.mouse.get_pos()來取得游標位置,並不需要萃取MOUSEMOTION事件。此外,有時候我們需要屏蔽特定事件,讓其功能無法使用(例如尚未達到取得的條件),此時可以使用set_block()方法,e.g. pygame.event.set_blocked(MOUSEMOTION)、pygame.event.set_blocked([KEYDOWN, KEYUP]),若是想要取消(unblock),傳入參數None即可(pygame.event.set_blocked(MOUSEMOTION)。與set_block()相反的函數為pygame.event.set_allowed(),不過若是輸入參數為None,實際上為block all events。在使用之前,可以先使用pygame.event.get_block()方法來取得目前被屏蔽的事件。 看個例子:
import sys
import pygame
from pygame.locals import *

sp = 'sheep.png'  # 置於與程式相同的資料夾
bg = 'background.png'  # 置於與程式相同的資料夾

ScreenSize = (800, 600)
pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(ScreenSize, RESIZABLE, 32)  # 建立window並設定大小
background = pygame.image.load(bg).convert()

font = pygame.font.SysFont("arial", 80)
text_surface = font.render("Game Message", True, (0, 0, 255))
x = ScreenSize[0]
y = (ScreenSize[1] - text_surface.get_height())/2

while True:  # 主程式迴圈
    for event in pygame.event.get():  # 事件的loop

        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            sys.exit()  # 退出系統

        if event.type == MOUSEBUTTONDOWN:
            if event.button == 3:  # 按下滑鼠右鍵
                pygame.event.set_blocked(MOUSEMOTION)

        elif event.type == MOUSEBUTTONUP:  # 必須有放開的事件,否則MOUSEMOTION不再有作用
            if event.button == 3:  # 放開滑鼠右鍵
                pygame.event.set_allowed(MOUSEMOTION)

        if event.type == MOUSEMOTION:  # MOUSEMOTION event可以不處理
            # 滑鼠必須持續移動,文字才會移動
            win.blit(background, (0, 0))
            x -= 2
            if x < -ScreenSize[0]:
                x = ScreenSize[0]
            win.blit(text_surface, (x, y))
            pygame.display.update()  # 更新畫面,須有此行否則無內容
跑馬燈
上例為了測試set_blocked()函數才那樣設計,事實上可以寫成如下的自動跑馬燈形式。
import sys
import pygame
from pygame.locals import *

sp = 'sheep.png'  # 置於與程式相同的資料夾
bg = 'background.png'  # 置於與程式相同的資料夾

ScreenSize = (800, 600)
pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(ScreenSize, RESIZABLE, 32)  # 建立window並設定大小
background = pygame.image.load(bg).convert()

font = pygame.font.SysFont("arial", 80)
text_surface = font.render("Game Message", True, (0, 0, 255))
x = ScreenSize[0]
y = (ScreenSize[1] - text_surface.get_height())/2  # 中間點

while True:  # 主程式迴圈
    for event in pygame.event.get():  # 事件的loop

        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            sys.exit()  # 退出系統

    win.blit(background, (0, 0))
    x -= 1
    if x < -ScreenSize[0]:
        x = ScreenSize[0]
    win.blit(text_surface, (x, y))
    pygame.display.update()  # 更新畫面,須有此行否則無內容

Draw

Pygame可以在視窗內繪製幾何圖形,首先先看怎麼設定pixels,可以在畫面上畫上小點。繪製時使用set_at()方法,可以輸入座標跟顏色。 <
from sys import exit
import pygame
from pygame.locals import *
import random as rd

ScreenSize = (800, 600)

def random_color():
    return rd.randint(0, 255), rd.randint(0, 255), rd.randint(0, 255)

def random_position():
    return rd.randint(0, ScreenSize[0]), rd.randint(0, ScreenSize[1])

pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(ScreenSize, RESIZABLE, 32)  # 建立window並設定大小

while True:  # 主程式迴圈
    for event in pygame.event.get():  # 事件的loop
        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            exit()  # 退出系統

        if event.type == WINDOWSHOWN:  # 視窗初始時
            win.lock()
            for _ in range(100):
                rand_col = random_color()
                rand_pos = random_position()
                win.set_at(rand_pos, rand_col)
            win.unlock()
        pygame.display.update()  # 更新畫面,須有此行否則無內容
除了繪製pixels之外,pygame.draw可以用來繪製多種圖形。
Rectangle
from sys import exit
import pygame
from pygame.locals import *
import random as rd

ScreenSize = (800, 600)

def random_color():
    return rd.randint(0, 255), rd.randint(0, 255), rd.randint(0, 255)

def random_position():
    return rd.randint(0, ScreenSize[0]), rd.randint(0, ScreenSize[1])

def random_size(x, y):
    return rd.randint(5, 20), rd.randint(5, 20)

pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(ScreenSize)  # 建立window並設定大小

while True:  # 主程式迴圈
    for event in pygame.event.get():  # 事件的loop
        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            exit()  # 退出系統

        if event.type == WINDOWSHOWN:  # 視窗初始時
            win.lock()
            for _ in range(20):
                color = random_color()
                pos = random_position()  # 左上角座標(x, y)
                size = random_size(pos[0], pos[1])  # 寬、高(w, h)
                line_width = rd.randint(0, 5)  # 0表示實心矩形
                pygame.draw.rect(win, color, pos+size, line_width)
                # pygame.draw.rect(win, color, Rect(pos, size), line_width)
            win.unlock()
        pygame.display.update()  # 更新畫面,須有此行否則無內容
  • 使用pygame.draw.rect(win, color, pos+size, line_width)函數繪製矩形。
  • 因為視窗圖片都是方形,所以pygame特別設計一個Rect物件,在繪製矩形的時候,第三個參數直接輸入Rect物件即可。
  • 最後一個參數是矩形外框線厚度,若值為0表示為實心矩形。
Polygon
from sys import exit
import pygame
from pygame.locals import *
import random as rd

ScreenSize = (800, 600)

def random_color():
    return rd.randint(0, 255), rd.randint(0, 255), rd.randint(0, 255)

def vertices()->list:
    """
    * 產生一個多邊形端點
    """
    edges = rd.randint(3, 10)
    points = []
    for _ in range(edges):
        x = rd.randint(0, ScreenSize[0])
        y = rd.randint(0, ScreenSize[1])
        points.append((x, y))
    return points

pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(ScreenSize)  # 建立window並設定大小
# points = []
while True:  # 主程式迴圈
    for event in pygame.event.get():  # 事件的loop
        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            exit()  # 退出系統

        if event.type == WINDOWSHOWN:  # 視窗初始時
            win.lock()
            for _ in range(10):
                color = random_color()
                line_width = rd.randint(0, 15)  # 0表示實心矩形
                pygame.draw.polygon(win, color, vertices(), line_width)
            win.unlock()
        pygame.display.update()  # 更新畫面,須有此行否則無內容
  • 跟矩形做法差不多,頂點的座標依順序置於list內做為第三個輸入參數即可。
Circle&Ellipse
from sys import exit
import pygame
from pygame.locals import *
import random as rd

ScreenSize = (800, 600)

def random_color():
    return rd.randint(0, 255), rd.randint(0, 255), rd.randint(0, 255)

def random_position():
    return rd.randint(0, ScreenSize[0]), rd.randint(0, ScreenSize[1])

pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(ScreenSize)  # 建立window並設定大小
# points = []
while True:  # 主程式迴圈
    for event in pygame.event.get():  # 事件的loop
        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            exit()  # 退出系統

        if event.type == WINDOWSHOWN:  # 視窗初始時
            win.lock()
            for _ in range(50):
                color = random_color()
                pos = random_position()  # 圓心座標(x, y)
                radius = rd.randint(5, 20)  # 半徑
                line_width = rd.randint(0, 15)  # 0表示實心
                pygame.draw.circle(win, color, pos, radius, line_width)
            win.unlock()
        pygame.display.update()  # 更新畫面,須有此行否則無內容
  • 使用pygame.draw.circle(win, color, pos, radius, line_width)繪製圓形。
  • 可以繪製橢圓形,同時亦可繪製圓形。
from sys import exit
import pygame
from pygame.locals import *
import random as rd

ScreenSize = (800, 600)

def random_color():
    return rd.randint(0, 255), rd.randint(0, 255), rd.randint(0, 255)

def random_position():
    return rd.randint(0, ScreenSize[0]), rd.randint(0, ScreenSize[1])

pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(ScreenSize)  # 建立window並設定大小
# points = []
while True:  # 主程式迴圈
    for event in pygame.event.get():  # 事件的loop
        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            exit()  # 退出系統

        if event.type == WINDOWSHOWN:  # 視窗初始時
            win.lock()
            for _ in range(50):
                color = random_color()
                pos = random_position()  # 圓心座標(x, y)
                axis = rd.randint(5, 20), rd.randint(5, 20)  # 軸, 相等為圓形
                line_width = rd.randint(0, 15)  # 0表示實心
                pygame.draw.ellipse(win, color, pos+axis, line_width)
            win.unlock()
        pygame.display.update()  # 更新畫面,須有此行否則無內容
Line&Arcs
from sys import exit
import pygame
from pygame.locals import *
import random as rd

ScreenSize = (800, 600)

def random_color():
    return rd.randint(0, 255), rd.randint(0, 255), rd.randint(0, 255)

pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(ScreenSize)  # 建立window並設定大小
ori_x, ori_y = None, None
while True:  # 主程式迴圈
    for event in pygame.event.get():  # 事件的loop
        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            exit()  # 退出系統

        if event.type == MOUSEBUTTONDOWN:
            if event.button == 1:  # 點擊滑鼠左鍵繪線
                x, y = pygame.mouse.get_pos()
                win.set_at((x, y), random_color())
                if ori_x is None and ori_y is None:
                    ori_x = x
                    ori_y = y
                else:
                    line_width = rd.randint(0, 10)
                    pygame.draw.line(win, random_color(), (ori_x, ori_y), (x,y), line_width)
                    ori_x = x
                    ori_y = y
            if event.button == 3:  # 點擊滑鼠右鍵取消
                ori_x = None
                ori_y = None

        pygame.display.update()  # 更新畫面,須有此行否則無內容
  • 使用pygame.draw.line(win, random_color(), (ori_x, ori_y), (x,y))繪製直線,
  • 試著將line替換為aaline(antialiasing)。
  • 繪製arc時,使用pygame.draw.arc(win, random_color(), (x, y, width, height), startAngle, endAngle, line_width)方法,與線段類似,只是需要曲率。
from sys import exit
import pygame
from pygame.locals import *
import random as rd
import math

ScreenSize = (800, 600)

def random_color():
    return rd.randint(0, 255), rd.randint(0, 255), rd.randint(0, 255)

pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(ScreenSize)  # 建立window並設定大小
ori_x, ori_y = None, None
while True:  # 主程式迴圈
    for event in pygame.event.get():  # 事件的loop
        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            exit()  # 退出系統

        if event.type == MOUSEBUTTONDOWN:
            if event.button == 1:  # 點擊滑鼠左鍵繪線
                x, y = pygame.mouse.get_pos()
                win.set_at((x, y), random_color())
                if ori_x is None and ori_y is None:
                    ori_x = x
                    ori_y = y
                else:
                    line_width = rd.randint(0, 10)
                    startAngle = math.radians(rd.randint(0, 360))
                    endAngle = math.radians(rd.randint(0, 360))
                    pygame.draw.arc(win, random_color(), (ori_x, ori_y, abs(x-ori_x), abs(y-ori_y)), startAngle, endAngle, line_width)
                    ori_x = x
                    ori_y = y
            if event.button == 3:  # 點擊滑鼠右鍵取消
                ori_x = None
                ori_y = None

        pygame.display.update()  # 更新畫面,須有此行否則無內容
  • 除了使用line繪製一條線,還可以使用pygame.draw.lines(win, random_color(), closed, points, line_width)來繪製折線,繪製時要給定一個list的點座標(points)。其中第三個參數closed是一個bool,若為True,表示最後會多劃一條頭尾相接的線來封閉折線區域,反之則否。
  • 試試看將lines替換成aalines。
from sys import exit
import pygame
from pygame.locals import *
import random as rd

ScreenSize = (800, 600)

def random_color():
    return rd.randint(0, 255), rd.randint(0, 255), rd.randint(0, 255)

pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(ScreenSize)  # 建立window並設定大小
x, y = None, None
points = [] ## 最多可以繪製100個點
closed = True  # 折線是否要會置封閉線段使其成多邊形
while True:  # 主程式迴圈
    for event in pygame.event.get():  # 事件的loop
        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            exit()  # 退出系統

        if event.type == MOUSEBUTTONDOWN:
            if event.button == 1:  # 點擊滑鼠左鍵繪線
                x, y = pygame.mouse.get_pos()
                points.append((x,y))
                win.set_at((x, y), random_color())
            if event.button == 2:  # 點擊滑鼠中鍵toggle closed
                closed = not closed
            if event.button == 3:  # 點擊滑鼠右鍵取消
                line_width = rd.randint(0, 10)
                pygame.draw.lines(win, random_color(), closed, points, line_width)
                points = []

        pygame.display.update()  # 更新畫面,須有此行否則無內容

Animation

遊戲難免要讓物件圖畫動起來,事實上在之前已經介紹過一些,e.g. 跑馬燈程式。現在先再試一個沿著一方向移動的圖片看看。
from sys import exit
import pygame
from pygame.locals import *

background = 'background.png' # 背景圖路徑(最好跟視窗同大小,800x600)
sheep = 'sheep.png' # 游標圖路徑(這個圖可以先修改小一點)
WinSize = (800, 600)
pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(WinSize) # 建立window並設定大小
pygame.display.set_caption("Hello") # 設定視窗標題

## 關於圖片
bg = pygame.image.load(background).convert() # 讀取背景圖
sp = pygame.image.load(sheep).convert_alpha() # 讀取游標圖
x = 0

while True: # 主程式迴圈
    for event in pygame.event.get(): #事件的loop
        if event.type == QUIT:
            pygame.quit() # 退出pygame
            exit() # 退出系統

    win.blit(bg, (0, 0))  # blit用來把被競圖渲染到視窗
    win.blit(sp, (x,400)) # 渲染至游標位置
    x += 1
    if x > WinSize[0]:
        x = 0
    pygame.display.update()  # 更新畫面,須有此行否則無內容

Clock

上例中的做法每一次會移動1個pixel(x+=1),不過移動速度跟電腦效能有關,效能高的走得快,效能低的走得慢。我們希望移動速動是固定的,此時需要使用時間來控制速度,因此,在此我們可以使用Clock()物件。
from sys import exit
import pygame
from pygame.locals import *

background = 'background.png' # 背景圖路徑(最好跟視窗同大小,800x600)
sheep = 'sheep.png' # 游標圖路徑(這個圖可以先修改小一點)
WinSize = (800, 600)
pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(WinSize) # 建立window並設定大小
pygame.display.set_caption("Hello") # 設定視窗標題

## 關於圖片
bg = pygame.image.load(background).convert() # 讀取背景圖
sp = pygame.image.load(sheep).convert_alpha() # 讀取游標圖
x, y = 0, 0  # 物件初始位置
clock = pygame.time.Clock()  # 定義控制時間的clock,用來追蹤時間(track time)
speedX = 300  # 設定x向的速度,pixels per second
speedY = 200  # 設定y向的速度,pixels per second

while True: # 主程式迴圈
    for event in pygame.event.get(): #事件的loop
        if event.type == QUIT:
            pygame.quit() # 退出pygame
            exit() # 退出系統
    ## 移至for loop之外,使其重複施作
    win.blit(bg, (0, 0))  # blit用來把被競圖渲染到視窗
    timePass = clock.tick()  # tick(framerate = 0) 更新clock -> 傳回 milliseconds
    passSecondes = timePass/1000  # 計算經過時間(s)
    movedX = passSecondes * speedX  # 計算x向位移
    movedY = passSecondes * speedY  # 計算y向位移
    x += movedX  # 計算新的x向位置
    y += movedY  # 計算新的y向位置
    if x > WinSize[0] - sp.get_width() or x < 0:  # 碰壁反彈
        speedX = -speedX
    if y > WinSize[1] - sp.get_height() or y < 0:  # 碰壁反彈
        speedY = -speedY
    win.blit(sp, (x, y))  # 渲染至游標位置
    pygame.display.update()  # 更新畫面,須有此行否則無內容
自由落體
根據上例的技能,設計一個模擬自由落體的程式。
from sys import exit
import pygame
from pygame.locals import *

WinSize = (1024, 1240)
pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(WinSize) # 建立window並設定大小
pygame.display.set_caption("Gravity") # 設定視窗標題

r = 20 # 園半徑
x = WinSize[0]/2  # 物件初始位置
h = 100  # start position >> from top to the center of circle
height = 100  # ball position (ball height)
black = (0, 0, 0)
red = (255, 0, 0)
clock = pygame.time.Clock()  # 定義控制時間的clock,用來追蹤時間(track time)
t = 0  # 經過的時間
while True: # 主程式迴圈
    for event in pygame.event.get(): #事件的loop
        if event.type == QUIT:
            pygame.quit() # 退出pygame
            exit() # 退出系統

    timePassed = clock.tick()  # tick(framerate = 0) 更新clock -> 傳回 milliseconds
    t += (timePassed / 1000)  # 計算經過時間(s)
    dh = 9.8*t*t/2  # (總位移)
    height = h + dh # 高度(自top往下算為h+)
    if WinSize[1] - height < r:  # 到達地面
        height = WinSize[1] - r  # 高度為園半徑(自top往下算為視窗高-半徑)
    win.fill(black)  # 先塗滿背景
    pygame.draw.circle(win, red, (x, height), r)  # 接著在新位置繪製圓形
    pygame.display.flip()  # 更新畫面,須有此行否則無內容

Collision Detection

要偵測兩個物品(或是游標與物品)是否接觸或碰撞(位置重疊),我們可以利用pygame.Rect物件來達成。先看一個簡單的例子:
from sys import exit
import pygame
from pygame.locals import *

WinSize = (800, 600)
pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(WinSize) # 建立window並設定大小
pygame.display.set_caption("Collision Detection") # 設定視窗標題
black = (0, 0, 0)
red = (255, 0, 0)
blue = (0, 0, 255)
color = red
r = 100 # 園半徑
x = WinSize[0]/2  # 物件初始位置
y = WinSize[1]/2  # 物件初始位置

while True: # 主程式迴圈
    for event in pygame.event.get(): #事件的loop
        if event.type == QUIT:
            pygame.quit() # 退出pygame
            exit() # 退出系統

    win.fill(black)  # 先塗滿背景
    circle = pygame.draw.circle(win, color, (x, y), r)  # 接著在新位置繪製圓形
    cursor = pygame.mouse.get_pos()  # get cursor position
    collision = circle.collidepoint(cursor)  # detect if collision happened
    color = blue if collision else red  # change color

    pygame.display.update()  # 更新畫面,須有此行否則無內容
有了點跟物件的偵測,接著嘗試物件跟物件的碰撞偵測。
from sys import exit
import pygame
from pygame.locals import *

WinSize = (800, 600)
pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(WinSize) # 建立window並設定大小
pygame.display.set_caption("Collision Detection") # 設定視窗標題

black = (0, 0, 0)
red = (255, 0, 0)
blue = (0, 0, 255)
green = (0, 255, 0)
grey = (100, 100, 100)
## red circle
color = red
center = (WinSize[0]/2, WinSize[1]/2)
r = 100 # 園半徑
## green ball
ballCenter = (100, 100)
ballColor = green
ballRadius = 20

drag = False


while True: # 主程式迴圈
    for event in pygame.event.get(): #事件的loop
        if event.type == QUIT:
            pygame.quit() # 退出pygame
            exit() # 退出系統

        if event.type == MOUSEBUTTONDOWN:  # when mouse button down
            if event.button == 1:  # press the left button
                if green_ball.collidepoint(event.pos):  # ball cursor collision
                    drag = True

        if event.type == MOUSEBUTTONUP:  # when mouse button up
            if event.button == 1:  # release the left button
                drag = False

        if event.type == MOUSEMOTION:  # on mouse motion
            if drag:  # when drag is True
                ballCenter = event.pos

    win.fill(black)  # 先塗滿背景

    circle = pygame.draw.circle(win, color, center, r)  # 接著在新位置繪製圓形
    green_ball = pygame.draw.circle(win, ballColor, ballCenter, ballRadius)

    collision = circle.colliderect(green_ball)  # detect if collision happened
    color = blue if collision else red  # change color

    pygame.display.update()  # 更新畫面,須有此行否則無內容
兩圖碰撞偵測
修改之前的單圖動畫,使其成為兩圖動畫並偵測碰撞,碰撞時反向移動。
from sys import exit
import pygame
from pygame.locals import *

background = 'background.png' # 背景圖路徑(最好跟視窗同大小,800x600)
sheep = 'sheep.png' # 游標圖路徑(這個圖可以先修改小一點)
WinSize = (800, 600)
pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(WinSize) # 建立window並設定大小
pygame.display.set_caption("Collision Detection") # 設定視窗標題

## 關於圖片
bg = pygame.image.load(background).convert() # 讀取背景圖
sp = pygame.image.load(sheep).convert_alpha() # 讀取移動圖
sp2 = pygame.image.load(sheep).convert_alpha() # 讀取移動圖
x1, y1 = 0, 0  # 物件1初始位置
x2, y2 = 0, WinSize[1] - sp2.get_height()  # 物件2初始位置

clock = pygame.time.Clock()  # 定義控制時間的clock,用來追蹤時間(track time)
speedX = 300  # 設定x向的速度(1),pixels per second
speedY = 200  # 設定y向的速度(1),pixels per second
speedX2 = 250  # 設定x向的速度(2),pixels per second
speedY2 = 350  # 設定y向的速度(2),pixels per second

while True: # 主程式迴圈
    for event in pygame.event.get(): #事件的loop
        if event.type == QUIT:
            pygame.quit() # 退出pygame
            exit() # 退出系統

    win.blit(bg, (0, 0))  # blit用來把被競圖渲染到視窗

    ## 移動物件1
    timePass = clock.tick()  # tick(framerate = 0) 更新clock -> 傳回 milliseconds
    passSecondes = timePass/1000  # 計算經過時間(s)
    movedX = passSecondes * speedX  # 計算x向位移
    movedY = passSecondes * speedY  # 計算y向位移
    x1 += movedX  # 計算新的x向位置
    y1 += movedY  # 計算新的y向位置
    if x1 > WinSize[0] - sp.get_width() or x1 < 0:  # 碰壁反彈
        speedX = -speedX
    if y1 > WinSize[1] - sp.get_height() or y1 < 0:  # 碰壁反彈
        speedY = -speedY

    ## 移動物件2
    movedX2 = passSecondes * speedX2  # 計算x向位移
    movedY2 = passSecondes * speedY2  # 計算y向位移
    x2 += movedX2  # 計算新的x向位置
    y2 += movedY2  # 計算新的y向位置
    if x2 > WinSize[0] - sp2.get_width() or x2 < 0:  # 碰壁反彈
        speedX2 = -speedX2
    if y2 > WinSize[1] - sp2.get_height() or y2 < 0:  # 碰壁反彈
        speedY2 = -speedY2

    ## 偵測碰種
    spRect = sp.get_rect(x=x1, y=y1)  # 取得圖片1之Rect物件(在當時位置)
    sp2Rect = sp2.get_rect(x=x2, y=y2)  # 取得圖片2之Rect物件(在當時位置)
    if spRect.colliderect(sp2Rect):  # 偵測碰撞
        speedX = -speedX
        speedY = -speedY
        speedX2 = -speedX2
        speedY2 = -speedY2

    win.blit(sp, (x1, y1))  # 渲染至游標位置
    win.blit(sp2, (x2, y2))  # 渲染至游標位置
    pygame.display.update()  # 更新畫面,須有此行否則無內容
多圖碰撞偵測
測試多個移動圖形相互碰撞的情形。
from sys import exit
import pygame
from pygame.locals import *
import random as rd

background = 'background.png' # 背景圖路徑(最好跟視窗同大小,800x600)
sheep = 'sheep.png' # 游標圖路徑(這個圖可以先修改小一點)
WinSize = (800, 600)
pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(WinSize) # 建立window並設定大小
pygame.display.set_caption("Collision Detection") # 設定視窗標題

## 關於圖片
bg = pygame.image.load(background).convert() # 讀取背景圖

class Sheep(object):
    def __init__(self, x=0, y=0, speed_x=0, speed_y=0):
        self.x = x
        self.y = y
        self.speed_x = speed_x
        self.speed_y = speed_y
        self.image = pygame.image.load(sheep).convert_alpha() # 讀取移動圖

    def new_pos(self, t):
        moved_x = t * self.speed_x  # 計算x向位移
        moved_y = t * self.speed_y  # 計算y向位移
        self.x += moved_x  # 計算新的x向位置
        self.y += moved_y  # 計算新的y向位置
        ## 四個方向分別控制,碰壁後回復位置與邊齊,跑起來比較順
        if self.x >= WinSize[0] - self.image.get_width():  # 碰壁反彈
            self.x = WinSize[0] - self.image.get_width()  # 回復位置
            self.speed_x = -self.speed_x
            ##增加速度方向變化,改用self.speed_x = -(self.speed_x+rd.randint(-10,10))
        if self.x <= 0:  # 碰壁反彈
            self.x = 0  # 回復位置
            self.speed_x = -self.speed_x
        if self.y >= WinSize[1] - self.image.get_height():# or self.y <= 0:  # 碰壁反彈
            self.y = WinSize[1] - self.image.get_height()  # 回復位置
            self.speed_y = -self.speed_y
            ##增加速度方向變化,改用self.speed_y = -(self.speed_y+rd.randint(-10,10))
        if self.y <= 0:  # 碰壁反彈
            self.y = 0  # 回復位置
            self.speed_y = -self.speed_y

    def rect(self):
        """
        return: image的Rect物件
        """
        return self.image.get_rect(x=self.x, y=self.y)

sp1 = Sheep(0, 0, 300, 200)
sp2 = Sheep(0, WinSize[1]-sp1.image.get_height(), 250, 350)
sp3 = Sheep(WinSize[0]-sp1.image.get_width(), 0, 300, 300)
sp4 = Sheep(WinSize[0]-sp1.image.get_width(), WinSize[1]-sp1.image.get_height(), 350, 250)
sp5 = Sheep(WinSize[0]/2, WinSize[1]/2, -125, 175)
sp6 = Sheep(50, 50, 500, 500)
sp7 = Sheep(50, 50, 400, 700)

# 隨機初始速度方向
# sp1 = Sheep(0, 0, rd.randint(50,375), rd.randint(50,375))
# sp2 = Sheep(0, WinSize[1]-sp1.image.get_height(), rd.randint(50,375), rd.randint(50,375))
# sp3 = Sheep(WinSize[0]-sp1.image.get_width(), 0, rd.randint(50,375), rd.randint(50,375))
# sp4 = Sheep(WinSize[0]-sp1.image.get_width(), WinSize[1]-sp1.image.get_height(), rd.randint(50,375), rd.randint(50,375))
# sp5 = Sheep(WinSize[0]/2, WinSize[1]/2, rd.randint(50,375), rd.randint(50,375))

flock = [sp1, sp2, sp3, sp4, sp5]

clock = pygame.time.Clock()  # 定義控制時間的clock,用來追蹤時間(track time)

while True: # 主程式迴圈
    for event in pygame.event.get(): #事件的loop
        if event.type == QUIT:
            pygame.quit() # 退出pygame
            exit() # 退出系統

    win.blit(bg, (0, 0))  # blit用來把被競圖渲染到視窗

    ## 移動物件1
    time_passed = clock.tick()  # tick(framerate = 0) 更新clock -> 傳回 milliseconds
    passSecondes = time_passed/1000  # 計算經過時間(s)

    for s in flock:  # update sheep position
        s.new_pos(passSecondes)

    ## 偵測碰種
    for i, s in enumerate(flock):
        for j, r in enumerate(flock):
            if i==j:
                continue
            else:
                if s.rect().colliderect(r.rect()):
                    ## 回復到碰撞前位置,感覺跑起來比較順
                    s.x -= passSecondes * s.speed_x
                    s.y -= passSecondes * s.speed_y
                    s.speed_x = -s.speed_x
                    s.speed_y = -s.speed_y

                    ##增加速度方向變化,改用
                    # s.speed_x = -(s.speed_x + rd.randint(-10, 10))
                    # s.speed_y = -(s.speed_y + rd.randint(-10, 10))

    for s in flock:  # update sheep position
        win.blit(s.image, (s.x, s.y))  # 渲染至游標位置
    pygame.display.update()  # 更新畫面,須有此行否則無內容
  • 因為有多個物件,建立class整理一下。
  • 在碰撞時,將物件位置回復至碰撞前,跑起來比較順,因為若沒這步驟,可能兩個物件已然重疊。增加sp6跟sp7測試,若是一開始即重疊很多,最後會卡在一起。
  • 碰撞後可以增加一點方向跟速度的變化。
  • 此處的碰撞依然採用colliderect()來偵測倆倆之間是否有碰撞。以下的例子使用collidelist()來偵測某rect與一個list內的所有rect是否有碰撞(僅修改上例中偵測兩圖碰撞的部分)。
from sys import exit
import pygame
from pygame.locals import *
import random as rd

background = 'background.png' # 背景圖路徑(最好跟視窗同大小,800x600)
sheep = 'sheep.png' # 游標圖路徑(這個圖可以先修改小一點)
WinSize = (800, 600)
pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(WinSize) # 建立window並設定大小
pygame.display.set_caption("Collision Detection") # 設定視窗標題

## 關於圖片
bg = pygame.image.load(background).convert() # 讀取背景圖

class Sheep(object):
    def __init__(self, x=0, y=0, speed_x=0, speed_y=0):
        self.x = x
        self.y = y
        self.speed_x = speed_x
        self.speed_y = speed_y
        self.image = pygame.image.load(sheep).convert_alpha() # 讀取移動圖

    def new_pos(self, t):
        moved_x = t * self.speed_x  # 計算x向位移
        moved_y = t * self.speed_y  # 計算y向位移
        self.x += moved_x  # 計算新的x向位置
        self.y += moved_y  # 計算新的y向位置
        ## 四個方向分別控制,碰壁後回復位置與邊齊,跑起來比較順
        if self.x >= WinSize[0] - self.image.get_width():  # 碰壁反彈
            self.x = WinSize[0] - self.image.get_width()  # 回復位置
            self.speed_x = -self.speed_x
            ##增加速度方向變化,改用self.speed_x = -(self.speed_x+rd.randint(-10,10))
        if self.x <= 0:  # 碰壁反彈
            self.x = 0  # 回復位置
            self.speed_x = -self.speed_x
        if self.y >= WinSize[1] - self.image.get_height():# or self.y <= 0:  # 碰壁反彈
            self.y = WinSize[1] - self.image.get_height()  # 回復位置
            self.speed_y = -self.speed_y
            ##增加速度方向變化,改用self.speed_y = -(self.speed_y+rd.randint(-10,10))
        if self.y <= 0:  # 碰壁反彈
            self.y = 0  # 回復位置
            self.speed_y = -self.speed_y

    def rect(self):
        """
        return: image的Rect物件
        """
        return self.image.get_rect(x=self.x, y=self.y)

sp1 = Sheep(0, 0, 300, 200)
sp2 = Sheep(0, WinSize[1]-sp1.image.get_height(), 250, 350)
sp3 = Sheep(WinSize[0]-sp1.image.get_width(), 0, 300, 300)
sp4 = Sheep(WinSize[0]-sp1.image.get_width(), WinSize[1]-sp1.image.get_height(), 350, 250)
sp5 = Sheep(WinSize[0]/2, WinSize[1]/2, -125, 175)
sp6 = Sheep(50, 50, 500, 500)
sp7 = Sheep(50, 50, 400, 700)

# 隨機初始速度方向
# sp1 = Sheep(0, 0, rd.randint(50,375), rd.randint(50,375))
# sp2 = Sheep(0, WinSize[1]-sp1.image.get_height(), rd.randint(50,375), rd.randint(50,375))
# sp3 = Sheep(WinSize[0]-sp1.image.get_width(), 0, rd.randint(50,375), rd.randint(50,375))
# sp4 = Sheep(WinSize[0]-sp1.image.get_width(), WinSize[1]-sp1.image.get_height(), rd.randint(50,375), rd.randint(50,375))
# sp5 = Sheep(WinSize[0]/2, WinSize[1]/2, rd.randint(50,375), rd.randint(50,375))

flock = [sp1, sp2, sp3, sp4, sp5]

clock = pygame.time.Clock()  # 定義控制時間的clock,用來追蹤時間(track time)

while True: # 主程式迴圈
    for event in pygame.event.get(): #事件的loop
        if event.type == QUIT:
            pygame.quit() # 退出pygame
            exit() # 退出系統

    win.blit(bg, (0, 0))  # blit用來把被競圖渲染到視窗

    ## 移動物件1
    time_passed = clock.tick()  # tick(framerate = 0) 更新clock -> 傳回 milliseconds
    passSecondes = time_passed/1000  # 計算經過時間(s)

    for s in flock:  # update sheep position
        s.new_pos(passSecondes)

    ## 偵測碰種 >> 使用collidelist()方法
    for i, s in enumerate(flock):
        others = [f.rect() for f in flock if s != f]
        if s.rect().collidelist(others) >= 0:
            s.x -= passSecondes * s.speed_x
            s.y -= passSecondes * s.speed_y
            s.speed_x = -s.speed_x
            s.speed_y = -s.speed_y

    for s in flock:  # update sheep position
        win.blit(s.image, (s.x, s.y))  # 渲染至游標位置
    pygame.display.update()  # 更新畫面,須有此行否則無內容

Exercise: 練習多個幾何圖形碰撞的偵測。


Sprits

在Pygame中,事實上已經有針對一個遊戲元件所定義的物件,稱為Sprite。我們可以直接繼承該物件來建立自己的Sprite,而且Sprite也內建碰撞偵測可供使用。現在將上例之多圖碰撞修改如下:
from sys import exit
import pygame
from pygame.locals import *
import random as rd

background = 'background.png' # 背景圖路徑(最好跟視窗同大小,800x600)
sheep = 'sheep.png' # 游標圖路徑(這個圖可以先修改小一點)
WinSize = (800, 600)
pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode(WinSize) # 建立window並設定大小
pygame.display.set_caption("Collision Detection") # 設定視窗標題

## 關於圖片
bg = pygame.image.load(background).convert() # 讀取背景圖

class Sheep(pygame.sprite.Sprite):  # 繼承Sprite
    def __init__(self, sid, x=0, y=0, speed_x=0, speed_y=0):
        super().__init__()
        self.sid = sid
        self.x = x
        self.y = y
        self.speed_x = speed_x
        self.speed_y = speed_y
        self.image = pygame.image.load(sheep).convert()  # 讀取背景圖,這是一個Surface物件
        self.rect = self.image.get_rect(x=self.x, y=self.y)  # 取得圖片的rect物件
        self.mask = pygame.mask.from_surface(self.image)  # 取得mask物件


    def update(self, t):
        moved_x = t * self.speed_x  # 計算x向位移
        moved_y = t * self.speed_y  # 計算y向位移
        self.x += moved_x  # 計算新的x向位置
        self.y += moved_y  # 計算新的y向位置
        self.rect.center = (self.x, self.y)  # <<<<<< 在此更新rect的位置
        ## 四個方向分別控制,碰壁後回復位置與邊齊,跑起來比較順
        if self.x >= WinSize[0] - self.image.get_width():  # 碰壁反彈
            self.x = WinSize[0] - self.image.get_width()  # 回復位置
            self.speed_x = -self.speed_x
            ##增加速度方向變化,改用self.speed_x = -(self.speed_x+rd.randint(-10,10))
        if self.x <= 0:  # 碰壁反彈
            self.x = 0  # 回復位置
            self.speed_x = -self.speed_x
        if self.y >= WinSize[1] - self.image.get_height():# or self.y <= 0:  # 碰壁反彈
            self.y = WinSize[1] - self.image.get_height()  # 回復位置
            self.speed_y = -self.speed_y
            ##增加速度方向變化,改用self.speed_y = -(self.speed_y+rd.randint(-10,10))
        if self.y <= 0:  # 碰壁反彈
            self.y = 0  # 回復位置
            self.speed_y = -self.speed_y

sp1 = Sheep(1, 0, 0, 300, 200)
sp2 = Sheep(2, 0, WinSize[1]-sp1.image.get_height(), 250, 350)
sp3 = Sheep(3, WinSize[0]-sp1.image.get_width(), 0, 300, 300)
sp4 = Sheep(4, WinSize[0]-sp1.image.get_width(), WinSize[1]-sp1.image.get_height(), 350, 250)
sp5 = Sheep(5, WinSize[0]/2, WinSize[1]/2, -125, 175)
sp6 = Sheep(6, 50, 50, 500, 500)
sp7 = Sheep(7, 50, 50, 400, 700)

# 隨機初始速度方向
# sp1 = Sheep(0, 0, rd.randint(50,375), rd.randint(50,375))
# sp2 = Sheep(0, WinSize[1]-sp1.image.get_height(), rd.randint(50,375), rd.randint(50,375))
# sp3 = Sheep(WinSize[0]-sp1.image.get_width(), 0, rd.randint(50,375), rd.randint(50,375))
# sp4 = Sheep(WinSize[0]-sp1.image.get_width(), WinSize[1]-sp1.image.get_height(), rd.randint(50,375), rd.randint(50,375))
# sp5 = Sheep(WinSize[0]/2, WinSize[1]/2, rd.randint(50,375), rd.randint(50,375))

flock = [sp1, sp2, sp3, sp4, sp5]

clock = pygame.time.Clock()  # 定義控制時間的clock,用來追蹤時間(track time)

while True: # 主程式迴圈
    for event in pygame.event.get(): #事件的loop
        if event.type == QUIT:
            pygame.quit() # 退出pygame
            exit() # 退出系統

    win.blit(bg, (0, 0))  # blit用來把被競圖渲染到視窗

    ## 移動物件1
    time_passed = clock.tick()  # tick(framerate = 0) 更新clock -> 傳回 milliseconds
    passSecondes = time_passed/1000  # 計算經過時間(s)

    for s in flock:  # update sheep position
        s.update(passSecondes)

    ## 偵測碰種 >> 使用pygame.sprite.spritecollide()方法
    for i, s in enumerate(flock):
        others = pygame.sprite.Group()
        for j, o in enumerate(flock):
            if i != j:
                others.add(o)
        ss = pygame.sprite.spritecollide(s, others, False, pygame.sprite.collide_mask)

        if ss:
            for z in ss:
                print(s.sid, z.sid)
            print("-------------")
            s.x -= passSecondes * s.speed_x
            s.y -= passSecondes * s.speed_y
            s.speed_x = -s.speed_x
            s.speed_y = -s.speed_y

    for s in flock:  # update sheep position
        win.blit(s.image, (s.x, s.y))
    pygame.display.update()  # 更新畫面,須有此行否則無內容
Kokiko
設計一個kokiko遊戲。
from sys import exit
import pygame
from pygame.locals import *
import random as rd

sheep = 'sheep.png' # 游標圖路徑(這個圖可以先修改小一點)

# colors
silver = (192, 192, 192)
black = (0, 0, 0)
red = (255, 0, 0)
brick_red = (183, 50, 57)
green = (0, 255, 0)
blue = (0, 0, 255)
white = (255, 255, 255)

# game settings
W = 1000  # window width
H = 800  # window height
BallW = 10  # ball width
BallH = 10  # ball height
BrickW = 50  # brick width
BrickH = 30  # brick height
ScoreArea = 80
Lives = 2
speed_increment = 10


# pygame settings
pygame.init() # 初始化,這步驟一定要有
win = pygame.display.set_mode((W,H)) # 建立window並設定大小
pygame.display.set_caption("Kokiko") # 設定視窗標題
sp = pygame.image.load(sheep).convert() # 讀取背景圖

class Ball(pygame.sprite.Sprite):  # 繼承Sprite
    score = 0
    def __init__(self, ball_id=0, ball_x=0, ball_y=0, speed_x=0, speed_y=0):
        super().__init__()
        self.ball_id = ball_id
        self.ball_x = ball_x
        self.ball_y = ball_y
        self.speed_x = speed_x
        self.speed_y = speed_y
        self.image = pygame.Surface((BallW, BallH))  # Surface((width, height))物件
        self.image.fill(silver)
        self.rect = self.image.get_rect(x=self.ball_x, y=self.ball_y)  # 取得圖片的rect物件
        self.mask = pygame.mask.from_surface(self.image)  # 取得mask物件
        self.running = True

    def draw(self, screen):
        screen.blit(self.image, (self.ball_x, self.ball_y))

    def update(self, t):
        moved_x = t * self.speed_x  # 計算x向位移
        moved_y = t * self.speed_y  # 計算y向位移
        self.ball_x += moved_x  # 計算新的x向位置
        self.ball_y += moved_y  # 計算新的y向位置
        self.rect.center = (self.ball_x, self.ball_y)  # <<<<<< 在此更新rect的位置
        ## 僅控制三面
        if self.ball_x >= W - BallW:  # 碰壁反彈
            self.ball_x = W - BallW  # 回復位置
            self.speed_x = -self.speed_x
            ##增加速度方向變化,改用self.speed_x = -(self.speed_x+rd.randint(-10,10))
        if self.ball_x <= 0:  # 碰壁反彈
            self.ball_x = 0  # 回復位置
            self.speed_x = -self.speed_x
        if self.ball_y >= H - BallH:  # or self.y <= 0:  # 碰壁反彈
            self.ball_y = H - BallH  # 回復位置
            self.speed_y = -self.speed_y
        if self.ball_y < 0:
            self.running = False
            ##增加速度方向變化,改用self.speed_y = -(self.speed_y+rd.randint(-10,10))

class Brick(pygame.sprite.Sprite):  # 繼承Sprite
    def __init__(self, x, y):
        super().__init__()
        self.x = x - BrickW/2
        self.y = y - BrickH/2
        self.image = pygame.Surface((BrickW, BrickH))  # Surface((width, height))物件
        self.image.fill(brick_red)
        # self.rect = self.image.get_rect(center=(self.x, self.y))  # 取得圖片的rect物件
        self.rect = self.image.get_rect(x=self.x, y=self.y)  # 取得圖片的rect物件
        self.mask = pygame.mask.from_surface(self.image)  # 取得mask物件

    def draw(self, screen):
        self.image.fill(brick_red)
        # screen.blit(self.image, self.rect.center)
        screen.blit(self.image, (self.x , self.y))

class CursorBrick(pygame.sprite.Sprite):  # 繼承Sprite
    def __init__(self):
        super().__init__()
        self.image = pygame.Surface((BrickW, BrickH))  # Surface((width, height))物件
        self.image.fill(green)
        # self.rect = self.image.get_rect(center=(0, 0))  # 取得圖片的rect物件
        self.rect = self.image.get_rect()  # 取得圖片的rect物件
        self.mask = pygame.mask.from_surface(self.image)  # 取得mask物件

    def draw(self, screen):
        # screen.blit(self.image, self.rect.center)
        screen.blit(self.image, (self.rect.x, self.rect.y))

    def update(self):
        mx, my = pygame.mouse.get_pos()
        self.rect.x = mx-BrickW/2
        self.rect.y = my-BrickH/2
        if my <= ScoreArea:
            self.image.fill(blue)

# ball
ball = Ball(ball_x=rd.randint(1, W-BallW-1), ball_y=H-BallW-1, speed_x=300, speed_y=-200)
# ball_group = pygame.sprite.GroupSingle(ball)
# brick
brick_group = pygame.sprite.Group()  # group
cursor_brick = CursorBrick()  # cursor brick
# score
score_font = pygame.font.SysFont(None, 60) # 設定字型

# clock
clock = pygame.time.Clock()  # 定義控制時間的clock,用來追蹤時間(track time)

while True:  # 主程式迴圈
    # redraw windown
    win.fill(black)
    game_score = score_font.render(f"Score: {Ball.score:.0f}", True, white)  # 回傳surface
    game_lives = score_font.render(f"Lives: {Lives}", True, white)  # 回傳surface
    win.blit(game_score, (10, 10))  # blit用來把其他元素渲染到另一個surface
    win.blit(game_lives, (W-10-game_lives.get_width(), 10))  # blit用來把其他元素渲染到另一個surface

    for event in pygame.event.get():  # 事件的loop
        if event.type == QUIT:
            pygame.quit()  # 退出pygame
            exit()  # 退出系統
        if event.type == MOUSEBUTTONDOWN:
            if event.button == 1 and ball.running:
                x, y = pygame.mouse.get_pos()
                if not ss and y > ScoreArea:
                    brick = Brick(x, y)
                    brick_group.add(brick)
                # print(f"ball speed: {ball.speed_x}, {ball.speed_y}")
            if event.button == 3:
                if not ball.running and Lives > 0:
                    ball = Ball(ball_x=rd.randint(1, W-BallW-1), ball_y=H-BallW-1, speed_x=300, speed_y=-200)
                    # ball_group = pygame.sprite.GroupSingle(ball)
                    Lives -= 1
        if event.type == KEYDOWN:
            if event.key == K_SPACE and ball.running:
                x, y = pygame.mouse.get_pos()
                if not ss and y > ScoreArea:
                    brick = Brick(x, y)
                    brick_group.add(brick)

        if event.type == WINDOWSHOWN:
            pygame.draw.rect(win, white, Rect((100, 100), (100, 100)), 1)

    # Calculate time
    time_passed = clock.tick()  # tick(framerate = 0) 更新clock -> 傳回 milliseconds
    pass_seconds = time_passed / 1000  # 計算經過時間(s)
    if ball.running:
        Ball.score += pass_seconds
        ## increase speed
        if Ball.score > 100:
            inc = Ball.score // 100
            # print("inc:", inc, speed_increment)
            if ball.speed_x > 0:
                ball.speed_x = 300 + inc * speed_increment
            else:
                ball.speed_x = -300 - inc * speed_increment
            if ball.speed_y > 0:
                ball.speed_y = 200 + inc * speed_increment
            else:
                ball.speed_y = -200 - inc * speed_increment

    # update ball & draw
    ball.update(pass_seconds)
    ball.draw(win)  # blit()

    # update cursor & draw
    cursor_brick.update()
    cursor_brick.draw(win)  # blit()

    # update bricks & draw
    for brick in brick_group:
        brick.update()

    # collision - ball > brick
    collisions = pygame.sprite.spritecollide(ball, brick_group, True, pygame.sprite.collide_mask)
    if collisions:
        ball_left = ball.ball_x - ball.speed_x * pass_seconds  ## ball left
        ball_top = ball.ball_y - ball.speed_y * pass_seconds  ## ball top
        brick_left = collisions[0].x - BrickW / 2  ## brick left
        brick_top = collisions[0].y - BrickH / 2  ## brick top

        ball_right = ball_left + BallW  ## ball right > |BallW|
        ball_bottom = ball_top + BallH  ## ball bottom > 工
        brick_right = brick_left + BrickW  ## brick right > |BrickW|
        brick_bottom = brick_top + BrickH  ## brick bottom > 工
        speed_slope = abs(ball.speed_y)/abs(ball.speed_x)
        ## figure out which side of brick is collided
        if ball.speed_x > 0 and ball.speed_y > 0:  # 球往右下
            if brick_top <= ball_bottom and brick_left >= ball_right:
                ball.speed_x = -ball.speed_x
            elif brick_top >= ball_bottom and brick_left <= ball_right:
                ball.speed_y = -ball.speed_y
            else:
                if (ball_bottom-brick_top)/(ball_right-brick_left) <= speed_slope:  # 位置斜率小於速度斜率
                    ball.speed_x = -ball.speed_x
                else:
                    ball.speed_y = -ball.speed_y
            # if brick_top > ball_bottom:
            #     if ball_right < brick_left:  ## brick top 低於 ball bottom
            #         if (ball_bottom-brick_top)/(ball_right-brick_left) < speed_slope:  # 位置斜率小於速度斜率
            #             ball.speed_x = -ball.speed_x
            #         else:
            #             ball.speed_y = -ball.speed_y
            #     else:
            #         ball.speed_y = -ball.speed_y
            # else:
            #     ball.speed_x = -ball.speed_x

        if ball.speed_x > 0 and ball.speed_y < 0:  # 球往右上
            if brick_bottom < ball_top:
                if ball_right < brick_left:  ## brick top 低於 ball bottom
                    if abs(ball_top - brick_bottom) / abs(ball_right - brick_left) < speed_slope:  # 位置斜率小於速度斜率
                        ball.speed_x = -ball.speed_x
                    else:
                        ball.speed_y = -ball.speed_y
                else:
                    ball.speed_y = -ball.speed_y
            else:
                ball.speed_x = -ball.speed_x

        if ball.speed_x < 0 and ball.speed_y > 0:  # 球往左下
            if brick_top > ball_bottom:
                if ball_left < brick_right:  ## brick top 低於 ball bottom
                    if abs(ball_bottom - brick_top) / abs(ball_left - brick_right) < speed_slope:  # 位置斜率小於速度斜率
                        ball.speed_x = -ball.speed_x
                    else:
                        ball.speed_y = -ball.speed_y
                else:
                    ball.speed_y = -ball.speed_y
            else:
                ball.speed_x = -ball.speed_x

        if ball.speed_x < 0 and ball.speed_y < 0:  # 球往左上
            if brick_bottom < ball_top:
                if ball_left < brick_right:  ## brick top 低於 ball bottom
                    if abs(ball_top - brick_bottom) / abs(ball_left - brick_right) < speed_slope:  # 位置斜率小於速度斜率
                        ball.speed_x = -ball.speed_x
                    else:
                        ball.speed_y = -ball.speed_y
                else:
                    ball.speed_y = -ball.speed_y
            else:
                ball.speed_x = -ball.speed_x

        Ball.score += 10

    # collision - cursor_brick > brick
    ss = pygame.sprite.spritecollide(cursor_brick, brick_group, False, pygame.sprite.collide_mask)
    if ss:
        cursor_brick.image.fill(blue)
        for s in ss:
            s.image.fill(white)
    else:
        cursor_brick.image.fill(green)

    if not ss:
        for brick in brick_group.sprites():
            brick.image.fill(brick_red)

    brick_group.draw(win)  # blit()
    pygame.display.update()  # 更新畫面,須有此行否則無內容
  • 使用sprite物件,並使用spritecollide()來判斷碰撞。
  • 球若跑出上方邊界算失敗,點擊(或按空白鍵)來放置磚塊。磚塊不能相互重疊。失敗後若還有lives,點擊右鍵重新開始。
  • 試了多個方法來判斷碰撞的方向(亦即是磚塊的哪一面受到撞擊),可惜效果並不很好,有些情況下還是沒有反轉。
-->