タイトル画像
琴葉葵

おねーちゃん!ちょっと気になってたんだけど、Pyxelでキャラクターの絵とかを細かく作ろうと思ったら、図形を組み合わせて作るしかないの?

琴葉茜

良い疑問だね!実は、今回ゲーム作りにPyxelを紹介した理由の1つが「それ」なんだ!

琴葉葵

え?そうなの??

琴葉茜

Pyxelにはドット絵や音楽を作るためのGUIエディタがあって、これがすごく簡単で便利なんだよ!

琴葉葵

ほうほう

琴葉茜

まぁこれに関しては、エディタを触ってみると凄さがよく分かると思うから、まずは起動してみようか!

琴葉茜

ターミナル上で、「hockey.py」が置かれているフォルダをカレントディレクトリにして、下のコマンドを実行してみてね

pyxel edit assets.pyxres
解説画像1
琴葉葵

!?!?なんか急に画面が現れた!?

琴葉茜

これがエディタ画面だよ!左の大きい枠は16x16のマスになっていて、右側の四角い白枠の範囲に描画されるんだ

琴葉茜

右側の部分が1枚の紙になっているイメージで、プログラムで扱う時はその紙のどの箇所(座標)の絵を使うかって命令の出し方をするんだ

解説画像2
琴葉葵

なるほどね!じゃあ、右下の「Image」って書かれているところの数字は何なの??

琴葉茜

その数字は「何枚目の紙か」を表しているんだ
今回はホッケーの絵くらいだからそのままで良いけど、例えば「キャラクターの画像」と「背景に使う木や雲などの画像」みたいなのは管理を分けたくなるでしょ

琴葉葵

確かに、そういう時に1枚の紙じゃなくて複数で分けられるのは便利だね!

琴葉茜

あと、色は全部で16色なんだけど、このうちの1色を「透過色」に指定が出来るから、基本的には15色でデザインすると思っておくと良いよ

琴葉葵

「透過色」?

琴葉茜

これについては画像をプログラムに反映するときに説明するよ

琴葉茜

ということで、とりあえずこんな感じでホッケーの絵を作ってみてね!
16x16の範囲内ならどんなデザインでもOKだよ!

琴葉葵

オッケー!完成したよ!!

解説画像3
琴葉茜

完成したら、Windowsなら「Ctrl + S」、Macなら「control + S」で保存してエディタを終了してね

琴葉茜

次は作った画像をプログラムに反映させていくよ!
まずは関数「__init__」で「assets.pyxres」を読み込む処理を追加しよう

def __init__(self):
    pyxel.init(160, 120, title="ホッケーゲーム")
    pyxel.load("assets.pyxres") # これを追加

    self.mode = "TITLE"

    self.reset_game()

    pyxel.run(self.update, self.draw)
琴葉葵

これが無いと作った素材が読み込めないんだね

琴葉茜

逆に言えば、これを読み込むだけで全部の素材の読み込みが完了するんだよ

琴葉葵

それは読み込み忘れが起きなくなるから良いね!

琴葉茜

次はホッケー画像を表示するために関数「draw」の「pyxel.circ()」を「pyxel.blt()」に変更するよ

# 関数「draw」内のホッケーの描画を変更
if self.mode == "PLAY": # プレイ画面
    # pyxel.circ(self.x, self.y, self.r, 8)
    pyxel.blt(self.x - self.r, self.y - self.r, 0, 0, 0, 16, 16, 0) # 作成したホッケー画像の表示
琴葉茜

画像の表示は「pyxel.blt(x, y, img, u, v, w, h, colkey)」となっていて、
x, y : 表示するx,y座標(左上が基準)
img : 何枚目の紙か
u, v : 読み込みたい画像の左上x,yの座標
w, h : 画像の幅と高さ
colkey : 透過したい色の番号
と引数を設定するよ

琴葉葵

なるほど、この「colkey」に指定した色が透明になるから、さっき「透過色」って言ってたんだね

琴葉葵

あと、x,y座標の指定が「pyxel.circ()」の時は円の中心だったのが「pyxel.blt()」だと左上になってるから、半径分だけズラしてるんだね!

琴葉茜

そういうこと
ちなみに、ホッケーの大きさが変わってるから、関数「reset_game」の「self.r」を変更しておこうね

# ホッケーの初期化
def reset_hockey(self):
    self.x = pyxel.width // 2
    self.y = pyxel.height // 2
    self.vx = random.choice([-1, 1])
    self.vy = random.choice([-1, 1])
    self.r = 8 # ここを変更
琴葉葵

そういえばそうだったね!ちゃんと修正しておくよ

解説画像4
琴葉葵

おおー、ちゃんとホッケーが作った画像に代わってる!

琴葉茜

さて、じゃあ次は「音」を追加してみようか

琴葉葵

そうか!なんか寂しいと思ったら音が無かったからだ!

琴葉茜

ということで、ホッケー画像を作った時と同じくエディタを起動しようか

琴葉葵

おっけー!起動したよ!

琴葉茜

じゃあ、画面上の左から3つ目のアイコンをクリックしてみてね

解説画像5
琴葉葵

なんか左にピアノの鍵盤が出てきた!

琴葉茜

これが「SOUND」のエディタ画面になるよ
そしたら、なんなく鍵盤の横の白い範囲をクリックして音楽を作ってみてね!クリックしたらピンク色のマーカーが付いて、白い範囲の一番下をクリックすると「休符」を意味する青色のマーカーが出てくるよ

解説画像6
琴葉葵

こんな感じ...かな??
下の黄色い場所は何なの??

琴葉茜

そこは、上から順に「トーン」「ボリューム」「エフェクト」を変更できる場所なんだ

項目 効果 備考
TON 音の種類
  • T:Triangle(三角波)
  • S:Square(矩形波)
  • P:Pulse(パルス波)
  • N:Noise(ノイズ)
VOL 音量
  • 0:無音
  • 7:最大音量
EFX 音の効果
  • N:None(効果なし)
  • S:Slide(滑らかにつなぐ)
  • V:Vibrato(音程を揺らす)
  • F:FadeOut(徐々に小さく)
  • H:Half-FadeOut(後半でフェード)
  • Q:Quarter-FadeOut(最後1/4でフェード)
琴葉茜

今回は触らずに続けるけど、もし気になるなら色々試してみてね!

琴葉葵

了解!

琴葉茜

それと、エディタの上部分の左から4つ目の音符アイコンの「MUSIC」を使えば、単音だけじゃなくて作った音を最大4つまで同時に鳴らすように組み合わせられるようになるよ

琴葉葵

単音だけじゃないんだ!じゃあ結構本格的なBGMも作れそうだね!

琴葉茜

音楽が作れたら保存してエディタを閉じよう
次は作った音楽をプログラムに反映させるよ!

# 関数「update」内の最初
if self.mode == "PLAY": # プレイ画面
    # BGM再生(チャンネル0でSOUND 0をループ)
    if not pyxel.play_pos(0):  # チャンネル0が再生中でなければ
        pyxel.play(0, 0, loop=True)
    
    # 以下省略
琴葉葵

お、ちゃんとプレイ画面に入ったら作った音楽が流れ始めた!

琴葉茜

単音の場合は「pyxel.play(チャンネル番号, SOUNDで作った音の番号, ループするかどうか)」って感じで設定できるよ

琴葉葵

チャンネル番号??MUSICのエディタ画面で最大4つまで音が鳴らせるって言ってたから、その番号のことかな

琴葉茜

おー察しが良いね!つまり、チャンネル番号に設定できるのは「0~3」になるから覚えておいてね

琴葉葵

おっけー!ちなみに、「MUSIC」で作った音を鳴らす場合はどうしたら良いの??

琴葉茜

その場合は、「pyxel.playm(MUSICで作った曲の番号, ループするかどうか)」で良いよ!

琴葉茜

あと、音楽を止める場合は「pyxel.stop()」を使えばOK!
今回はゲームが終了した時に音楽を止めるようにしよう

# 関数「update」内の最初
elif self.mode == "GAMEOVER": # 終了画面
    pyxel.stop() # これを追加
    if pyxel.btnp(pyxel.KEY_SPACE):
        self.mode = "TITLE"
琴葉葵

ゲームが終わったらちゃんとBGMが止まるようになった!

琴葉茜

バッチリだね!他にも、ホッケーとバーが衝突した時や得点が入った時にちょっとした音が鳴るようにしてみると良いかもね

琴葉葵

ちょっと自分なりに色々改良してみる!

琴葉茜

ひとまずホッケーゲームはこれで完成だよ!最後に、今回のホッケーをちょっと改良してみたデータを置いておくから、もし気になったら見てみてね

完成したプログラム

import pyxel
import random

class App:
    def __init__(self):
        pyxel.init(160, 120, title="ホッケーゲーム")
        pyxel.load("assets.pyxres")

        self.mode = "TITLE"

        self.reset_game()

        pyxel.run(self.update, self.draw)

    def reset_game(self):
        # ホッケーの初期化
        self.reset_hockey()

        # プレイヤー(左側)のバー
        self.bar_height = 20 # バーの高さ
        self.bar_width = 5 # バーの幅
        self.bar_x = 10 # プレイヤーのx座標
        self.bar_y = (pyxel.height - self.bar_height) // 2 # プレイヤーのy座標

        # CPU(右側)のバー
        self.cpu_x = pyxel.width - 10 - self.bar_width # CPUのx座標
        self.cpu_y = (pyxel.height - self.bar_height) // 2 # CPUのy座標

        # スコア用
        self.score_player = 0
        self.score_cpu = 0

    # ホッケーの初期化
    def reset_hockey(self):
        self.x = pyxel.width // 2
        self.y = pyxel.height // 2
        self.vx = random.choice([-1, 1])
        self.vy = random.choice([-1, 1])
        self.r = 8

    def update(self):
        if self.mode == "PLAY": # プレイ画面
            # BGM再生(チャンネル0でSOUND 0をループ)
            if not pyxel.play_pos(0):  # チャンネル0が再生中でなければ
                pyxel.play(0, 0, loop=True)
            self.x += self.vx
            self.y += self.vy
            
            # プレイヤーバーの操作(上下キー)
            if pyxel.btn(pyxel.KEY_UP):
                self.bar_y -= 2
            if pyxel.btn(pyxel.KEY_DOWN):
                self.bar_y += 2

            self.bar_y = max(0, min(pyxel.height - self.bar_height, self.bar_y))

            # CPUバーの自動追尾
            if random.random() > 0.2:  # 80%の確率で追尾
                if self.y > self.cpu_y + self.bar_height // 2:
                    self.cpu_y += 1.2
                elif self.y < self.cpu_y + self.bar_height // 2:
                    self.cpu_y -= 1.2
            self.cpu_y = max(0, min(pyxel.height - self.bar_height, self.cpu_y))

            # ウィンドウの端に当たったらリセット
            if self.x <= self.r: # 左側が当たった場合はCPUの得点
                self.score_cpu += 1
                self.reset_hockey()
            elif self.x >= pyxel.width - self.r:
                self.score_player += 1 # 右側が当たった場合はプレイヤーの得点
                self.reset_hockey()
            if self.y <= self.r or self.y >= pyxel.height - self.r:
                self.vy *= -1

            # 勝利条件チェック
            if self.score_player >= 5 or self.score_cpu >= 5:
                self.mode = "GAMEOVER"

            # プレイヤーバーの衝突判定
            if (
                self.x + self.r >= self.bar_x and
                self.x - self.r <= self.bar_x + self.bar_width and
                self.y + self.r >= self.bar_y and
                self.y - self.r <= self.bar_y + self.bar_height
            ):
                self.vx = abs(self.vx) + 1

            # CPUの衝突判定
            if (
                self.x - self.r <= self.cpu_x + self.bar_width and
                self.x + self.r >= self.cpu_x and
                self.y + self.r >= self.cpu_y and
                self.y - self.r <= self.cpu_y + self.bar_height
            ):
                self.vx = -(abs(self.vx) + 1)
        elif self.mode == "TITLE": # タイトル画面
            # スペースで開始
            if pyxel.btnp(pyxel.KEY_SPACE):
                self.reset_game()
                self.mode = "PLAY"
        elif self.mode == "GAMEOVER": # 終了画面
            pyxel.stop()
            if pyxel.btnp(pyxel.KEY_SPACE):
                self.mode = "TITLE"

    def draw(self):
        pyxel.cls(0)
        if self.mode == "PLAY": # プレイ画面
            # pyxel.circ(self.x, self.y, self.r, 8)
            pyxel.blt(self.x - self.r, self.y - self.r, 0, 0, 0, 16, 16, 0)
            pyxel.rect(self.bar_x, self.bar_y, self.bar_width, self.bar_height, 11)
            pyxel.rect(self.cpu_x, self.cpu_y, self.bar_width, self.bar_height, 14)

            pyxel.text(20, 4, f"YOU: {self.score_player:04}", 11)
            pyxel.text(100, 4, f"CPU: {self.score_cpu:04}", 14)
        elif self.mode == "TITLE": # タイトル画面
            pyxel.text(40, 40, "--- HOCKEY GAME ---", 7)
            pyxel.text(40, 80, "PRESS SPACE TO START", 6)
        elif self.mode == "GAMEOVER": # 終了画面
            if self.score_player > self.score_cpu:
                winner = "YOU"  
            else:
                winner = "CPU"
            pyxel.text(60, 40, f"{winner} WIN!!", 10)
            pyxel.text(20, 70, "PRESS SPACE TO RETURN TO TITLE", 7)

App()