Hello World / plɹoM ollǝH

Programmers Live in Vain

PySide ドラッグでデータ受け渡し

f:id:dungeonneko:20170525143550g:plain

import sys
from PySide import QtCore, QtGui


# ドラッグ&ドロップで色を受け渡す
class WidgetItem(QtGui.QLabel):
    def __init__(self, color):
        super().__init__('')
        self._color = color
        self.setStyleSheet('border: 1px solid black;')
        self.setFixedSize(32, 32)
        self.setAcceptDrops(True)

    def paintEvent(self, event):
        self.changeColor()
        super().paintEvent(event)

    def changeColor(self):
        pal = QtGui.QPalette(self.palette())
        pal.setColor(QtGui.QPalette.Window, QtGui.QColor(self._color))
        self.setAutoFillBackground(True)
        self.setPalette(pal)

    def mousePressEvent(self, event):
        # ドラッグオブジェクトに色データ(テキスト)を埋め込んで実行
        drag = QtGui.QDrag(self)
        mime = QtCore.QMimeData()
        mime.setText(self._color)
        drag.setMimeData(mime)
        drag.exec_()

    def dragEnterEvent(self, event):
        event.accept()

    def dragMoveEvent(self, event):
        event.accept()

    def dropEvent(self, event):
        if event.mimeData().hasText():
            self._color = event.mimeData().text()
            self.changeColor()


app = QtGui.QApplication(sys.argv)

widget = QtGui.QWidget()
widget.setFixedSize(640, 240)
widget.setLayout(QtGui.QHBoxLayout())
layout = widget.layout()
layout.addWidget(WidgetItem('red'))
layout.addWidget(WidgetItem('blue'))
layout.addWidget(WidgetItem('green'))
layout.addWidget(WidgetItem('white'))
widget.show()
sys.exit(app.exec_())

PySide ドラッグでWidgetを移動させる

f:id:dungeonneko:20170525134629g:plain

import sys
from PySide import QtCore, QtGui

# ドラッグで動くWidget
class WidgetItem(QtGui.QLabel):
    def __init__(self):
        super().__init__('')
        self.setStyleSheet('border: 1px solid black; background-color: red;')
        self.setFixedSize(32, 32)
        self._drag = False
        self._offset = QtCore.QPoint(0, 0)

    def mousePressEvent(self, event):
        self._drag = True
        self._offset = event.pos()

    def mouseReleaseEvent(self, event):
        self._drag = False

    def mouseMoveEvent(self, event):
        if self._drag:
            self.move(self.mapToParent(event.pos() - self._offset))

app = QtGui.QApplication(sys.argv)

widget = QtGui.QWidget()
widget.setFixedSize(640, 480)
widget.setLayout(QtGui.QVBoxLayout())
layout = widget.layout()
item = WidgetItem()
layout.addWidget(item)
widget.show()
sys.exit(app.exec_())

Pythonゲームプログラミング #9 スクロール

gist消してしまったので
代わりにこのリポジトリを参考にしてください

#0 環境構築
#1 メインループ
#2 画像表示
#3 アニメーション
#4 キー入力
#5 サウンド
#6 衝突判定
#7 ステートマシン
#8 マップチップ
#9 スクロール

スクロール

pygameを使ったゲームプログラミングについて説明していきます。

  • Windows10
  • Python3.6.1
  • pygame1.9.3

今回はスクロールについて紹介します。

スクロールとは?

画面に収まりきらない空間をカメラを移動させることで表示する技法です。 実際は描画位置を「カメラからの相対位置」に変換することで実現しています。

f:id:dungeonneko:20170519211651p:plain

サンプルコード

前回のマップを拡げて画面に収まらないようにします。 ついでに上下左右キーで移動できるカメラを追加します。

実行結果

f:id:dungeonneko:20170519210628g:plain

Pythonゲームプログラミング #8 マップチップ

gist消してしまったので
代わりにこのリポジトリを参考にしてください

#0 環境構築
#1 メインループ
#2 画像表示
#3 アニメーション
#4 キー入力
#5 サウンド
#6 衝突判定
#7 ステートマシン
#8 マップチップ
#9 スクロール

マップチップ

pygameを使ったゲームプログラミングについて説明していきます。

  • Windows10
  • Python3.6.1
  • pygame1.9.3

今回はマップチップについて説明します。

マップチップの概念

マップチップは背景のパターンをデータ化したものです。マップチップのデータはキャラクターの通行可否の判定などにも使用できます。 画像を再利用して大きなマップを作成することで、作業の手間を省くことも出来ますし、少ないデータで広大なマップを表現することができます。

f:id:dungeonneko:20170518230548p:plain

また、チップ単位でデータを持つという考えは、マップデータだけでなくゲームオブジェクトの配置などにも応用が可能です。 座標系やオブジェクトのサイズを固定することで、衝突判定やAIの処理負荷を下げることも出来ます。

サンプルコード

画像データ(chip.png)

f:id:dungeonneko:20170518225949p:plain

実行結果

f:id:dungeonneko:20170518230001p:plain

Pythonゲームプログラミング #7 ステートマシン

gist消してしまったので
代わりにこのリポジトリを参考にしてください

#0 環境構築
#1 メインループ
#2 画像表示
#3 アニメーション
#4 キー入力
#5 サウンド
#6 衝突判定
#7 ステートマシン
#8 マップチップ
#9 スクロール

ステートマシン

pygameを使ったゲームプログラミングについて説明していきます。

  • Windows10
  • Python3.6.1
  • pygame1.9.3

今回はステートマシンです。

ステートマシンって何?

簡単にいうと「状態(ステート)によって振る舞いを変化させたり、状態を遷移させる仕組み」のことです。 この仕組みをゲームプログラミングにうまく適用することで、複雑なプログラムも簡潔に書けるようになり、バグも減ります。

例えば、ゲームに時限爆弾を登場させたいときには「爆発まで待機する状態」「爆発している状態」「爆発が終わった状態」などが考えられます。 それぞれの状態では再生されるアニメーションもバラバラでしょうし「爆発の待機中はキャラクターが重ならないようにあたり判定を付けたい」 「爆発したときは攻撃のあたり判定を発生させたい」「爆発が終わったらゲームから消去したい」などの、状態の遷移と紐づいた様々な処理を実装する必要が出てきます。

f:id:dungeonneko:20170517215503p:plain

これを真面目にif文などで実装してしまうととても複雑なプログラムになってしまうので 「状態Aに入ったときの処理」「状態Aのあいだ呼ばれる処理」「状態Aから出ていくときの処理」などを関数で定義できるようにして、どの処理がいつ呼ばれるかはステートマシンクラスに管理させることにします。

サンプルコード

実行結果

待機ステート開始
爆発まであと90フレーム
爆発まであと89フレーム
爆発まであと88フレーム
.
.
.
爆発まであと3フレーム
爆発まであと2フレーム
爆発まであと1フレーム
待機ステート終了
爆発ステート開始
爆発アニメーションの終了待ち
爆発アニメーションの終了待ち
爆発アニメーションの終了待ち
爆発ステート終了
全ての処理が終了

ゲームオブジェクトだけでなくAIや

f:id:dungeonneko:20170517222616p:plain

シーン遷移などにもステートマシンを適用させることが可能です

f:id:dungeonneko:20170517222629p:plain

Pythonゲームプログラミング #6 衝突判定

gist消してしまったので
代わりにこのリポジトリを参考にしてください

#0 環境構築
#1 メインループ
#2 画像表示
#3 アニメーション
#4 キー入力
#5 サウンド
#6 衝突判定
#7 ステートマシン
#8 マップチップ
#9 スクロール

衝突判定

pygameを使ったゲームプログラミングについて説明していきます。

  • Windows10
  • Python3.6.1
  • pygame1.9.3

今回は衝突判定です。

バウンディングボリューム

f:id:dungeonneko:20170516141252p:plain:w480

ゲームプログラミングではフレーム処理をなるべく時間をかけずに終わらせる必要があるため、 ゲーム内のオブジェクトの衝突判定はバウンディングボリュームとよばれる幾何学形状に簡略化しておこなわれることがほとんどです。 今回は最も簡単な衝突判定の一つである円と円の判定を実装してみます。

サンプルコード

実行結果

f:id:dungeonneko:20170516161654g:plain:w320

解説

f:id:dungeonneko:20170516141317p:plain

画像を見てもわかるとおり、円と円が衝突しているかは「2つの円の中心距離」が「2つの円の半径の和」以下かどうかで判定することができます。 2つの円の半径と位置はパラメータで管理しますが、距離は計算で求める必要があります。 2点の座標間の距離は高校生で習う「ベクトル」というものを使うと簡単に求められるのですが、pygameは2Dなのでピタゴラスの定理で説明します。

f:id:dungeonneko:20170516141337p:plain

2Dの直交座標系上では、2点間の距離は直角三角形の斜辺の長さと同じものとして考えることができます。 ピタゴラスの定理より「斜辺の長さの2乗(c^2)」は「底辺の長さの2乗(a^2)と高さの2乗(b^2)の和」と等しい、つまり

c^2 = a^2 + b^2

という式が成り立ちます。平方根を求めるのは計算コストがかかるので、比較する半径の和も2乗して考えると、 円と円の衝突判定の式は下記のように「半径の和の2乗」と「斜辺の2乗」を比較したもので実現することができます。

r = r0 + r1  # 二つの半径の和
a = x0 - x1  # 底辺(2乗して判定するのでマイナスでも大丈夫)
b = y0 - y1  # 高さ(2乗して判定するのでマイナスでも大丈夫)

# 斜辺の長さが半径以下なら?
if (a * a + b * b) <= (r * r):
    print('当たってる')
else:
    print('当たってない')
個人的におすすめの衝突判定関連書籍

Pythonゲームプログラミング #5 サウンド

gist消してしまったので
代わりにこのリポジトリを参考にしてください

#0 環境構築
#1 メインループ
#2 画像表示
#3 アニメーション
#4 キー入力
#5 サウンド
#6 衝突判定
#7 ステートマシン
#8 マップチップ
#9 スクロール

サウンド

pygameを使ったゲームプログラミングについて説明していきます。

  • Windows10
  • Python3.6.1
  • pygame1.9.3

今回はサウンド処理です。

ゲームに必要な音について

ゲームに使うキー入力のパターンは大きく分けて2つあります。「効果音」と「BGM」です。

f:id:dungeonneko:20170515113724p:plain

pygameではどちらも簡単に実装できます。

サンプルコード

解説

初期化

5行目

pygame.mixer.pre_init(44100, -16, 1, 512)

第一引数:再生周波数
第二引数:ビット数(マイナスの場合はsigned, プラスの場合はunsigned)
第三引数:チャンネル数(1=モノラル、2=ステレオ)
第四引数:バッファサイズ(2の倍数)

pygame.initでmixerも初期化されるため、pre_init関数を使ってあらかじめ初期化の設定をしておきます。 バッファサイズは小さすぎるとサウンドが途切れがちになり、大きすぎると再生までのタイムラグが長くなります。 詳しくはドキュメントを読んでください。

効果音のロードと再生

11行目と26行目

se = pygame.mixer.Sound('se.wav')
se.play()

Soundクラスのドキュメントはこちら

BGMのロードと再生

14~16行目

pygame.mixer.music.load('bgm.wav')
pygame.mixer.music.set_volume(0.5)
pygame.mixer.music.play(-1)

playに渡してる引数はループ回数で-1は無限ループになります(Soundクラスでも同様にループ指定できます)。 musicのドキュメントはこちら

補足:メモリとストリーミング

ところで効果音とBGMのちがいは何でしょう?音の長さや使用する目的もちがいますが、プログラム上ではBGMを効果音として鳴らすこともできるわけです。 それなのに、なぜプログラムの書き方が違うかというと、現代のコンピュータには、メモリ容量という制限があるためです。

f:id:dungeonneko:20170515113758p:plain

コンピュータが色々な処理をするには、メモリ上に絵や音やプログラムを載せておかなければなりません。 ところがメモリの大きさは決まっていて、一度に載せることができる限度というものがあります。 ここで問題になるのがファイルサイズの大きい音楽や、映像などのデータです。

f:id:dungeonneko:20170515113816p:plain

大きなデータはそのままではメモリに載せることができなかったり、他のプログラムなどが載せられる場所が少なくなってしまうという問題があります。 そこで人類が考えたのがストリーミングという技術です。

f:id:dungeonneko:20170515113832p:plain

音楽全体はメモリに載せることができなくても「今聴いているところ」のデータだけをファイルから切り出してくれば、場所をとらずにメモリに載せることができるというわけです。