座標の回転と綺麗な配置方法について

タグ:

ゲームで自機の周囲にバリヤーやオプションを回転させたい場合、考えないといけない事がいくつか出てきます。

  • 自分の周りにオブジェクトを回転させるにはどうやるの?
  • 等間隔に綺麗に配置させたい場合はどうすれば良いの?

といった事です。

gistでプログラムを公開しているのですが、公開されているものは様々な手法が組み合わされている為、ここでは分解して説明してみます。

自分の周りにオブジェクトを回転させるには

「自分の周りをぐるぐると回る」という状態は、回したい物に糸を結んで、その糸を振り回している状態に近いと言えるでしょう。

プログラムで似たような事をやるには回転を行う計算が必要になりますが、いきなり回転の話をする前に単純なプログラムを順番に拡張していきます。(既にわかっている部分は読み飛ばして下さい)

横256、縦240ドットの画面をもつ何もしないプログラム

手始めにこのプログラムから始めます。実行すると何もしないWindowが表示されます。

import pyxel

SCREEN_W = 256
SCREEN_H = 240


def main():

    pyxel.init(SCREEN_W, SCREEN_H)

    while True:
        pyxel.cls(0)
        pyxel.flip()


if __name__ == "__main__":
    main()
単純に黒い画面が表示されます。

回したい長さの線を描く

プログラムに少し手を加えて、回したい長さの線を引きます。今回は長さが50の白い線を引くことにします。

import pyxel

SCREEN_W = 256
SCREEN_H = 240


def main():

    pyxel.init(SCREEN_W, SCREEN_H)

    while True:
        pyxel.cls(0)

        # (0, 0)から長さが50の白い線(50, 0)をひく
        x = 50
        y = 0
        pyxel.line(0, 0, x, y, 7)
        pyxel.flip()


if __name__ == "__main__":
    main()
判りづらいですが画面の左上に線が表示されます。

線はひけましたが、画面の左上に描画されてしまっています。

Pyxelは画面の左上を0, 0として、右に行くに従ってX座標が増加し、下に行くに従ってY座標が増加します。つまり右下が(255, 239)となります。

このままでもプログラムを動かすことは出来ますが、見やすくする為に画面の中央に移動させます。

画面の中央から線が引かれるように、それぞれの座標に画面の中央座標(128, 120)を足します。

import pyxel

SCREEN_W = 256
SCREEN_H = 240
CENTER_X = SCREEN_W // 2
CENTER_Y = SCREEN_H // 2


def main():

    pyxel.init(SCREEN_W, SCREEN_H)

    while True:
        pyxel.cls(0)

        # 長さが50の白い線(50, 0)をひく
        x = 50
        y = 0
        pyxel.line(CENTER_X + 0, CENTER_Y + 0, CENTER_X + x, CENTER_Y + y, 7)
        pyxel.flip()


if __name__ == "__main__":
    main()
位置を補正したので画面の中心から線画表示されます。

線を回転させる

回転をさせるには行列による計算が必要となりますが、ここでは数学を理解することが目的ではありませんので回転を実現する関数を用意してしまいます。

詳しく知りたい場合は、二次元 回転行列あたりで検索すると出てきます。

rotation関数

def rotation(x, y, p_by):
    """原点を軸に x, y を p_by 回転させます。

    Args:
        x (float):
            回転元のX座標
        y (float):
            回転元のY座標
        p_by (float):
            回転角度(ラジアン)

    Retrurns:
        x, y の回転結果をタプルで戻します。
    """
    sv = math.sin(p_by)
    cv = math.cos(p_by)
    return (x * cv - y * sv, x * sv + y * cv)

rotation関数の使い方

回転させたい座標をx, yに代入して、p_byに時計回りに回転させたい角度を指定します。角度はラジアンで指定するのですが、Pythonには角度をradianに変換する関数が用意されていますので、その関数を使用すれば度数で指定することが出来ます。

戻り値は入力座標を回転させたx, y座標となります。

使用例:座標(50, 0)を90度回転させる

x, y = rotation(50, 0, math.radians(90))

実際に90度回転させてみる

それでは実際にrotation関数を組み込んで回転をさせてみます。

座標(50, 0)を時計回りに回転させますので、計算結果は座標(0, 50)となります。

import math
import pyxel

SCREEN_W = 256
SCREEN_H = 240
CENTER_X = SCREEN_W // 2
CENTER_Y = SCREEN_H // 2


def rotation(x, y, p_by):
    """原点を軸に x, y を p_by 回転させます。

    Args:
        x (float):
            回転元のX座標
        y (float):
            回転元のY座標
        p_by (float):
            回転角度(ラジアン)

    Retrurns:
        x, y の回転結果をタプルで戻します。
    """
    sv = math.sin(p_by)
    cv = math.cos(p_by)
    return (x * cv - y * sv, x * sv + y * cv)


def main():

    pyxel.init(SCREEN_W, SCREEN_H)

    while True:
        pyxel.cls(0)

        # 長さが50の白い線(50, 0)をひく
        x = 50
        y = 0
        pyxel.line(CENTER_X + 0, CENTER_Y + 0, CENTER_X + x, CENTER_Y + y, 7)

        # (50, 0)を90度回転させて、青い線をひく
        rx, ry = rotation(x, y, math.radians(90))
        pyxel.line(CENTER_X + 0, CENTER_Y + 0, CENTER_X + rx, CENTER_Y + ry, 6)

        pyxel.flip()


if __name__ == "__main__":
    main()
元の線と90度回転した線を描画。

なりましたでしょうか。

ぐるぐる回してみる

回転出来ることがわかりました。あとは度数を変えてあげれば時計回りにぐるぐると回ってくれます。

import math
import pyxel

SCREEN_W = 256
SCREEN_H = 240
CENTER_X = SCREEN_W // 2
CENTER_Y = SCREEN_H // 2


def rotation(x, y, p_by):
    """原点を軸に x, y を p_by 回転させます。

    Args:
        x (float):
            回転元のX座標
        y (float):
            回転元のY座標
        p_by (float):
            回転角度(ラジアン)

    Retrurns:
        x, y の回転結果をタプルで戻します。
    """
    sv = math.sin(p_by)
    cv = math.cos(p_by)
    return (x * cv - y * sv, x * sv + y * cv)


def main():

    pyxel.init(SCREEN_W, SCREEN_H)

    # 回転角度
    angle = 0

    while True:
        pyxel.cls(0)

        # 長さが50の白い線(50, 0)をひく
        x = 50
        y = 0
        pyxel.line(CENTER_X + 0, CENTER_Y + 0, CENTER_X + x, CENTER_Y + y, 7)

        # (50, 0)をangle度回転させて、青い線をひく
        rx, ry = rotation(x, y, math.radians(angle))
        pyxel.line(CENTER_X + 0, CENTER_Y + 0, CENTER_X + rx, CENTER_Y + ry, 6)

        # 回転角度を1度加算(数値を変えれば回転速度が変わります)
        angle += 1

        pyxel.flip()


if __name__ == "__main__":
    main()
青い線が時計回りに回転しながら描画。

無事に画面を中心に回転を行う事が出来る様になりました。

等間隔に綺麗に配置させたい

回転が出来たことで次の問題はオブジェクトが複数個ある場合です。ひとつだけの場合は先程の方法でぐるぐると回せるのですが、複数個の場合はすこし工夫が必要です。

今回の場合は「等間隔に配置したい」ですから、等間隔で配置されるようにしてみましょう。

等間隔で配置するには同じ角度で配置する事を意味しますので、一回転、つまり360度を個数で割ることで綺麗に配置できます。

  • 回転するオブジェクトが0個 … 考えなくてよい(何も表示するものがないから)
  • 回転するオブジェクトが1個 … 360 / 1 -> 360度に一つ表示
  • 回転するオブジェクトが2個 … 360 / 2 -> 180度毎に一つ表示
  • 回転するオブジェクトが3個 … 360 / 3 -> 120度毎に一つ表示
  • 回転するオブジェクトが4個 … 360 / 4 -> 90度毎に一つ表示

こんな感じになります。

オブジェクトを3つを等間隔で配置させたい場合は120度毎に配置すれば良いことがわかります。

実際にプログラムで確認してみます。

import math
import pyxel

SCREEN_W = 256
SCREEN_H = 240
CENTER_X = SCREEN_W // 2
CENTER_Y = SCREEN_H // 2


def rotation(x, y, p_by):
    """原点を軸に x, y を p_by 回転させます。

    Args:
        x (float):
            回転元のX座標
        y (float):
            回転元のY座標
        p_by (float):
            回転角度(ラジアン)

    Retrurns:
        x, y の回転結果をタプルで戻します。
    """
    sv = math.sin(p_by)
    cv = math.cos(p_by)
    return (x * cv - y * sv, x * sv + y * cv)


def main():

    pyxel.init(SCREEN_W, SCREEN_H)

    while True:
        pyxel.cls(0)

        # 長さが50の白い線(50, 0)をひく
        x = 50
        y = 0
        pyxel.line(CENTER_X + 0, CENTER_Y + 0, CENTER_X + x, CENTER_Y + y, 7)

        # (50, 0)をangle度回転させて、青い線をひく
        # 一つ目は0度
        x = 50
        y = 0
        rx, ry = rotation(x, y, math.radians(0))
        pyxel.line(CENTER_X + 0, CENTER_Y + 0, CENTER_X + rx, CENTER_Y + ry, 6)

        x = 50
        y = 0
        # 二つ目は120度
        rx, ry = rotation(x, y, math.radians(120))
        pyxel.line(CENTER_X + 0, CENTER_Y + 0, CENTER_X + rx, CENTER_Y + ry, 6)

        x = 50
        y = 0
        # 三つ目は240度
        rx, ry = rotation(x, y, math.radians(240))
        pyxel.line(CENTER_X + 0, CENTER_Y + 0, CENTER_X + rx, CENTER_Y + ry, 6)

        pyxel.flip()


if __name__ == "__main__":
    main()
三つの線を等間隔に描画。

ここまで来れば回転させるのは簡単でしょう。先程回転させたようにrotationに与える角度を少しづつ加算してあげればよいです。

import math
import pyxel

SCREEN_W = 256
SCREEN_H = 240
CENTER_X = SCREEN_W // 2
CENTER_Y = SCREEN_H // 2


def rotation(x, y, p_by):
    """原点を軸に x, y を p_by 回転させます。

    Args:
        x (float):
            回転元のX座標
        y (float):
            回転元のY座標
        p_by (float):
            回転角度(ラジアン)

    Retrurns:
        x, y の回転結果をタプルで戻します。
    """
    sv = math.sin(p_by)
    cv = math.cos(p_by)
    return (x * cv - y * sv, x * sv + y * cv)


def main():

    pyxel.init(SCREEN_W, SCREEN_H)

    # 回転角度
    angle = 0

    while True:
        pyxel.cls(0)

        # 長さが50の白い線(50, 0)をひく
        x = 50
        y = 0
        pyxel.line(CENTER_X + 0, CENTER_Y + 0, CENTER_X + x, CENTER_Y + y, 7)

        # (50, 0)をangle度回転させて、青い線をひく
        # 一つ目は0度
        x = 50
        y = 0
        rx, ry = rotation(x, y, math.radians(0 + angle))
        pyxel.line(CENTER_X + 0, CENTER_Y + 0, CENTER_X + rx, CENTER_Y + ry, 6)

        x = 50
        y = 0
        # 二つ目は120度
        rx, ry = rotation(x, y, math.radians(120 + angle))
        pyxel.line(CENTER_X + 0, CENTER_Y + 0, CENTER_X + rx, CENTER_Y + ry, 6)

        x = 50
        y = 0
        # 三つ目は240度
        rx, ry = rotation(x, y, math.radians(240 + angle))
        pyxel.line(CENTER_X + 0, CENTER_Y + 0, CENTER_X + rx, CENTER_Y + ry, 6)

        # 回転角度を1度加算(数値を変えれば回転速度が変わります)
        angle += 1

        pyxel.flip()


if __name__ == "__main__":
    main()
等間隔に引いた線を回転させながら描画。

このプログラムでは回転しているのを判りやすく線をひいていますが、線を引かずに計算した座標に別の物を描画してあげれば好きな物を回転させることが出来ます。

ただこれでは、オブジェクトが3つのときだけしか対応出来ません。個数が増減しても動作するようにまとめてみます。

import math
import pyxel

SCREEN_W = 256
SCREEN_H = 240
CENTER_X = SCREEN_W // 2
CENTER_Y = SCREEN_H // 2


def rotation(x, y, p_by):
    """原点を軸に x, y を p_by 回転させます。

    Args:
        x (float):
            回転元のX座標
        y (float):
            回転元のY座標
        p_by (float):
            回転角度(ラジアン)

    Retrurns:
        x, y の回転結果をタプルで戻します。
    """
    sv = math.sin(p_by)
    cv = math.cos(p_by)
    return (x * cv - y * sv, x * sv + y * cv)


def main():

    pyxel.init(SCREEN_W, SCREEN_H)

    # 回転角度
    angle = 0
    # この数値を変えるとその数の分だけ等間隔で線が描画されます。
    object_count = 3

    while True:
        pyxel.cls(0)

        # 長さが50の白い線(50, 0)をひく
        x = 50
        y = 0
        pyxel.line(CENTER_X + 0, CENTER_Y + 0, CENTER_X + x, CENTER_Y + y, 7)

        # (50, 0)をangle度回転させて、青い線をひく
        for n in range(object_count):
            # 何度毎に配置するかを計算
            deg_base = 360 / object_count
            # オブジェクト毎の角度をdeg_baseを元に計算
            deg = deg_base * n
            x = 50
            y = 0
            rx, ry = rotation(x, y, math.radians(deg + angle))
            pyxel.line(CENTER_X + 0, CENTER_Y + 0, CENTER_X + rx, CENTER_Y + ry, 6)

        # 回転角度を1度加算(数値を変えれば回転速度が変わります)
        angle += 1

        pyxel.flip()


if __name__ == "__main__":
    main()

object_countの数を変えれば、好きな数のオブジェクトを回転させる事が出来るでしょう。

これだけでも動作するのですが、実際のゲームでオブジェクトを増減させると、角度が急に切り替わってしまうため、増減のタイミングで一瞬カクつきが発生します。

これを防ぐには、現在の角度からちょっとづつ目的の角度に変更する事で滑らかに見せることが出来ます。

実際にオブジェクトの増減を含めたデモ

"""プレイヤーの周囲を回転するオプションのサンプル
"""
import math
import pyxel
SCREEN_W = 256
SCREEN_H = 240
GAME_FPS = 60
MIN_RADIUS = 10
MAX_RADIUS = 100
DEFAULT_RADIUS = 30
FORMATION_SPEED = 0.02
ROT_SPEED = 4
def rotation(x: float, y: float, p_by: float) -> (float, float):
"""原点を軸に x, y を p_by 回転させます。
Args:
x (float):
回転元のX座標
y (float):
回転元のY座標
p_by (float):
回転角度(ラジアン)
Retrurns:
x, y の回転結果をタプルで戻します。
"""
sv = math.sin(p_by)
cv = math.cos(p_by)
return (x * cv y * sv, x * sv + y * cv)
class CPlayer:
def __init__(self):
self.x = 0
self.y = 0
class CPlayerOption:
def __init__(self, formation):
self.x = 0
self.y = 0
self.formation = formation
def change_formation(self, new_formation: int):
v = new_formation self.formation
if v != 0:
v /= abs(v)
self.formation += v * FORMATION_SPEED
def main():
pyxel.init(SCREEN_W, SCREEN_H, fps=GAME_FPS)
pyxel.mouse(False)
player = CPlayer()
list_option = []
radius = DEFAULT_RADIUS
while True:
# プレイヤーキャラクターの更新
# (オプションの回転座標の中心となります)
player.x = pyxel.mouse_x
player.y = pyxel.mouse_y
# マウス左クリックでオプションの追加。
# マウス右クリックでオプションの削除。
if pyxel.btnp(pyxel.MOUSE_LEFT_BUTTON) == 1:
list_option.append(CPlayerOption(len(list_option) + 1))
if pyxel.btnp(pyxel.MOUSE_RIGHT_BUTTON) == 1:
list_option = list_option[0:1]
# マウスホイールで回転半径の変更。
radius = min(max(radius + pyxel.mouse_wheel, MIN_RADIUS), MAX_RADIUS)
pyxel.cls(0)
for n, o in enumerate(list_option):
# オプションの個数にあわせて配置場所を計算します。
deg = 360 / o.formation * n
# オプション位置の回転処理。
x, y = rotation(
radius, 0, math.radians(deg + pyxel.frame_count * ROT_SPEED)
)
# オプション数が急に変わってもカクつかないように、徐々に角度を変更するための処理。
o.change_formation(len(list_option))
# プレイヤーの位置にあわせて、オプションの描画を行います。
pyxel.circ(player.x + x, player.y + y, 2, 6)
pyxel.circ(player.x, player.y, 3, 7)
pyxel.text(8, 8, "Option(s): {:7.2f}".format(len(list_option)), 7)
pyxel.text(8, 16, "Radius(s): {:7.2f}".format(radius), 7)
pyxel.flip()
if __name__ == "__main__":
main()
# [EOF]