PYXEL GUIDE / COLLISION
Pyxelの当たり判定の作り方
「プレイヤーがコインに触れたら取る」「敵にぶつかったらミス」「壁の中には入れない」。こうした処理をまとめて当たり判定と呼びます。Pyxelでは当たり判定を自分で書きますが、基本の形を4つ覚えれば、たいていのゲームに対応できます。
- わかること
- 四角・円・マス目・タイルマップの当たり判定と、すり抜けを防ぐコツ
- 前提
- 変数・if・関数と、Pyxelの update と draw の役割
- 確認したバージョン
- Pyxel 2.9.9

まずは形を決める
当たり判定は、2つの物がぶつかったか(重なったか)を調べる処理です。「コインに触れたら取る」「敵に当たったらミス」「壁の中には入れない」など、ほとんどのゲームで使います。
Pyxelには、キャラクターどうしの当たり判定を自動でしてくれる機能はありません。そのかわり、キャラクターを「四角」や「円」のような単純な形とみなして、数字で調べます。見た目の絵より少し小さめの形にすると、「当たっていないのにミスになった」と感じにくくなります。
| 形 | 調べ方 | 向いているもの |
|---|---|---|
| 四角 | 横の範囲と縦の範囲が、両方重なっているか | プレイヤー・アイテム・ブロック |
| 円 | 中心どうしの距離が、半径の合計より短いか | 弾・丸い敵・爆発の範囲 |
| マス目 | 行き先のマスが壁かどうか | 迷路・パズル・RPGの移動 |
| タイルマップ | Tilemap.collide に任せる | Pyxel Editor で作ったステージ |
四角と四角
pyxel.rect(x, y, w, h, col) は、左上が (x, y)、幅 w・高さ h の四角を描きます。当たり判定でも同じ4つの数で四角を表すと、描く四角と判定する四角をそろえやすくなります。
def rects_overlap(ax, ay, aw, ah, bx, by, bw, bh):
return ax < bx + bw and bx < ax + aw and ay < by + bh and by < ay + ahA の左はしが B の右はしより左にあり、B の左はしが A の右はしより左にある。これで「横の範囲が重なっている」ことがわかります。縦も同じように調べて、両方とも重なっているときだけ True を返します。どちらか1つでも離れていれば、当たっていません。
rects_overlap(0, 0, 4, 4, 3, 3, 4, 4) は True(右下の1ドットが重なる)、rects_overlap(0, 0, 4, 4, 4, 0, 4, 4) は False です。2つ目は、幅4の四角が x=0 と x=4 にすき間なく並んでいるだけで、重なってはいません。
< を <= に変えると、辺がぴったり接しただけでも当たりになります。どちらにするかは、ゲームに合わせて決めましょう。
円と円
2つの円は、中心どうしの距離が「2つの半径の合計」より短ければ重なっています。距離を求めるには平方根(√)の計算が必要ですが、両方を2乗して比べれば、平方根を使わずにすみます。
def circles_overlap(ax, ay, ar, bx, by, br):
dx = ax - bx
dy = ay - by
return dx * dx + dy * dy < (ar + br) ** 2import math をして math.hypot(dx, dy) < ar + br と書いても、同じ意味です。読みやすいほうを選んでかまいません。
円と四角を比べるとき
丸いボールと四角いブロックのように、円と四角を比べるときは、四角の中で円の中心にいちばん近い点を求めて、その点までの距離を調べます。講座「ころがる切手」で、切手(円)とタイルの壁を判定している方法です。
def circle_hits_rect(cx, cy, r, x, y, w, h):
nearest_x = max(x, min(cx, x + w))
nearest_y = max(y, min(cy, y + h))
dx = cx - nearest_x
dy = cy - nearest_y
return dx * dx + dy * dy < r * rマス目の壁
迷路やパズルのように、マップをマス目で作るときは、「動いたあとで壁から戻す」より「動く前に、行き先が壁でないかを調べる」ほうがかんたんです。マップは、文字を並べたリストで書けます。# が壁、. が床です。
TILE = 8 # 1マスの大きさ(ドット)
SIZE = 6 # プレイヤーの四角の大きさ
MAP = [
"####################",
"#..................#",
"#..####......####..#",
# …(全部で14行。サンプルの全文を参照)
]def is_wall(px, py):
return MAP[py // TILE][px // TILE] == "#"
def hits_wall(x, y, w, h):
return (is_wall(x, y) or is_wall(x + w - 1, y)
or is_wall(x, y + h - 1) or is_wall(x + w - 1, y + h - 1))is_wall は、ピクセルの位置をマス目の位置に変えて(// は割り算の答えの整数部分)、そのマスが # かどうかを調べます。hits_wall は、四角の4つの角のどれかが壁のマスに入っていないかを調べます。
4つの角だけを調べる方法は、キャラクターの四角が1マス(ここでは8ドット)以下のときに使えます。1マスより大きいキャラクターは、辺の途中が壁に入ることがあるので、調べる点を増やします。
横と縦を別々に動かす
dx = pyxel.btn(pyxel.KEY_RIGHT) - pyxel.btn(pyxel.KEY_LEFT)
dy = pyxel.btn(pyxel.KEY_DOWN) - pyxel.btn(pyxel.KEY_UP)
if not hits_wall(self.x + dx, self.y, SIZE, SIZE):
self.x += dx
if not hits_wall(self.x, self.y + dy, SIZE, SIZE):
self.y += dy横の移動と縦の移動を別々に調べると、ななめに進んで壁に当たったときも、壁にそってすべるように動けます。Pythonでは True を 1、False を 0 として計算できるので、右キーだけなら dx は 1、左キーだけなら -1、両方押すと 0 になります。
マス目の番号と画面のピクセルの変換は、共通入門の画面とマップの座標でくわしく練習できます。
タイルマップの壁(Tilemap.collide)
タイルマップ(8×8ドットのタイルを並べたステージ。Pyxel Editor などで作ります)を使っている場合は、Pyxel 2.6.5 で追加された Tilemap.collide を使えます。動かしたい量(dx, dy)を渡すと、壁にめりこまないように調整した量を返してくれます。
WALLS = [(1, 0), (2, 0)] # 壁として扱うタイルの種類(イメージバンク上の列, 行)
def push_back(x, y, dx, dy):
dx, dy = pyxel.tilemaps[0].collide(x, y, 8, 8, dx, dy, WALLS)
return x + dx, y + dyx, y, 8, 8は、キャラクターの四角(ピクセル)です。WALLSには、壁として扱うタイルの種類を、イメージバンクの上での位置(何列目, 何行目)で並べます。マップ上の場所ではない点に注意しましょう。- 公式サンプルの 10_platformer.py でも、この形でプレイヤーや敵を壁から押し戻しています。
ある場所に置かれたタイルを調べるだけなら、pget を使います。ピクセルの位置から調べるときは、8で割ってタイルの位置に変えます。
tile = pyxel.tilemaps[0].pget(x // 8, y // 8)
if tile in WALLS:
...collide と pget は、タイルマップを用意してから使う機能です。タイルマップは、Pyxel Editor(pyxel edit)で作るほか、文字列から作ったり、Tiled というツールのTMXファイルを読み込んだりして用意できます。この記事のサンプルは、追加のファイルなしで動くように、文字のリストのマップ(ひとつ前の方法)を使っています。
3つをまとめたサンプル
四角・円・マス目の3つの判定を、1本にまとめたサンプルです。矢印キーで黄緑の四角を動かし、黄色のコインを5枚集めるとクリアです。左右に動く赤い丸に当たると、スタート位置に戻ります。R キーで最初からやり直せます。
import pyxel
TILE = 8
SIZE = 6
MAP = [
"####################",
"#..................#",
"#..####......####..#",
"#..................#",
"#.....#......#.....#",
"#.....#......#.....#",
"#..................#",
"#..####......####..#",
"#..................#",
"#..................#",
"#..####......####..#",
"#..................#",
"#..................#",
"####################",
]
START = (12, 12)
COINS = [(28, 28), (124, 28), (76, 44), (28, 92), (124, 92)]
def is_wall(px, py):
return MAP[py // TILE][px // TILE] == "#"
def hits_wall(x, y, w, h):
return (is_wall(x, y) or is_wall(x + w - 1, y)
or is_wall(x, y + h - 1) or is_wall(x + w - 1, y + h - 1))
def rects_overlap(ax, ay, aw, ah, bx, by, bw, bh):
return ax < bx + bw and bx < ax + aw and ay < by + bh and by < ay + ah
def circles_overlap(ax, ay, ar, bx, by, br):
dx = ax - bx
dy = ay - by
return dx * dx + dy * dy < (ar + br) ** 2
class App:
def __init__(self):
pyxel.init(160, 120, title="Collision Demo")
self.reset()
pyxel.run(self.update, self.draw)
def reset(self):
self.x, self.y = START
self.coins = list(COINS)
self.enemy_x = 40
self.enemy_vx = 1
self.misses = 0
def update(self):
if pyxel.btnp(pyxel.KEY_R):
self.reset()
if not self.coins:
return
dx = pyxel.btn(pyxel.KEY_RIGHT) - pyxel.btn(pyxel.KEY_LEFT)
dy = pyxel.btn(pyxel.KEY_DOWN) - pyxel.btn(pyxel.KEY_UP)
if not hits_wall(self.x + dx, self.y, SIZE, SIZE):
self.x += dx
if not hits_wall(self.x, self.y + dy, SIZE, SIZE):
self.y += dy
for coin in self.coins[:]:
if rects_overlap(self.x, self.y, SIZE, SIZE, coin[0], coin[1], 4, 4):
self.coins.remove(coin)
self.enemy_x += self.enemy_vx
if self.enemy_x < 20 or self.enemy_x > 140:
self.enemy_vx = -self.enemy_vx
center_x = self.x + SIZE / 2
center_y = self.y + SIZE / 2
if circles_overlap(center_x, center_y, 3, self.enemy_x, 68, 4):
self.x, self.y = START
self.misses += 1
def draw(self):
pyxel.cls(0)
for row, line in enumerate(MAP):
for col, cell in enumerate(line):
if cell == "#":
pyxel.rect(col * TILE, row * TILE, TILE, TILE, 5)
for cx, cy in self.coins:
pyxel.rect(cx, cy, 4, 4, 10)
pyxel.circ(self.enemy_x, 68, 4, 8)
pyxel.rect(self.x, self.y, SIZE, SIZE, 11)
pyxel.text(4, 113, f"COINS:{len(self.coins)} MISS:{self.misses}", 7)
if not self.coins:
pyxel.text(60, 56, "CLEAR!", 7)
App().venv\Scripts\python.exe collision_demo.py.venv/bin/python collision_demo.py| 判定 | 使っている関数 | どうなる? |
|---|---|---|
| 壁 | hits_wall | 行き先が壁なら、その方向には動かない |
| コイン | rects_overlap | 重なったコインをリストから消す |
| 赤い丸 | circles_overlap | プレイヤーを半径3の円とみなして調べ、当たったらスタートへ戻す |
for coin in self.coins[:]: の [:] は、リストのコピーを作ってから順番に見ています。ループの途中で remove しても、コインの取りこぼしが起きないようにするためです。
赤い丸の半径を4から8にすると、当たりやすくなります。pyxel.circ の 4 と、circles_overlap に渡している 4 を両方変えて、見た目と判定をそろえましょう。片方だけ変えると、見た目と当たる範囲がずれることも確かめられます。
すり抜け・めりこみを防ぐコツ
速い物は、すり抜けることがある
1フレームで動く量が、壁や相手の大きさより大きいと、前のフレームでは手前、次のフレームでは向こう側にいて、一度も重ならないことがあります。これを「すり抜け」といいます。速く動く物は、1フレームの移動を何回かに分けて調べるか、通った線全体を調べます。講座「反射便」では、まっすぐ進む物が最初に壁に当たる時刻を求めて、すり抜けを防いでいます。
動く前に調べると、めりこまない
先に動かしてから調べると、壁の中に入った状態が1フレームだけ生まれます。このページのように「動く前に行き先を調べる」か、collide のように「動く量を調整する」と防げます。
判定は update で、draw では描くだけ
当たり判定は update の中で行い、draw では結果を描くだけにします。Pyxelは、処理が重いときに draw をとばすことがあります。draw の中で判定すると、当たったのに見逃すことがあるので注意しましょう。
講座で当たり判定を読む
このサイトの講座では、ゲームに合わせていろいろな当たり判定を使っています。やさしい順に読んでみましょう。
この記事で確認したこと
Pyxel 2.9.9 のAPIリファレンス(rect・circ・Tilemap.collide・Tilemap.pget)、変更履歴(collide は 2.6.5 で追加)、collide のソースコード(タイルは8ドット)、公式サンプル 10_platformer.py、よくある質問(draw がとばされることがある)を照合しました。
collision_demo.py は、Pyxelの代わりに入力と描画命令を記録する模擬モジュールで動かし、壁を通り抜けずに5枚すべてのコインを集められること、赤い丸に当たるとスタートに戻ること、判定の関数の境目(接しただけは False)を確かめています。実際のPyxelのウィンドウでの表示確認と、collide を使うタイルマップの例の実行は、この記事の作成時には行っていません。
Pyxelの使い方ガイド
ほかのガイドも、講座の前後にあわせて読めます。