Python何も知らない完全初心者だけどPyxelでクリスマスっぽいゲームを作りたい

## 紹介

Pythonのことは何も知らない人です。

今回の記事はPythonという言語に対してこうなのかななどの無駄な感想も適宜入っています。間違えて解釈してしまっている箇所もあるかと思いますのでご容赦ください…。

## 対象
- Pythonのことが分からない人

- pyxelを触ったことがないので概要を掴みたい人

## なんでをゲーム作りたい？
少し前にPyxelがQiitaで盛り上がっていたのを見て、一度はゲームを作ってみたい気持ちになってしまった

## Pythonについてどのくらいの知識か
- AIとか機械学習にいいらしい
- 一番人気ありそうなイメージ
- 図書館に行くとPythonの本は大体貸出中
- 型ないらしい（型ヒント的なのはあるらしい？）

→ 無知

## 完成品

有識者の方々に本記事をお読みいただくのは心苦しいため完成品だけでもご覧ください

- 　移　動：十字キー
- ジャンプ：SPACE

https://kitao.github.io/pyxel/wasm/launcher/?run=ojagao.sample-pyxel.christmas-game.main


## 早速ドキュメントを読んでみる

https://github.com/kitao/pyxel/blob/main/docs/README.ja.md

これを見ても最初はよくわからなかったのでハンズオンでとりあえず触ってみる。（初心者が躓かずにハンズオンできるので分かりやすかったです）

## 何をベースにクリスマスっぽいゲームを作るのかを決めてみる
READMEに様々な作例が載っていましたが、私が人生で初めて遊んだゲームはスーパーマリオなので横スクロールアクション的なのをベースとして作りたいと思います。

## READMEに書いてることをやってみる前に
pythonはMacには標準で入っているらしいですが念の為確認

```sh
python --version

# pythonっていうコマンド使えないよエラー
```
```sh
python3 --version

Python 3.9.6
```

過去に何かのパッケージを使用する為にPythonの新しめのバージョンが必要になって、標準で入っているPythonと自分で入れたバージョンのPythonで標準搭載Pythonが優先的に使用されてしまう問題を解決したような記憶もありますが正直覚えていないです、すみません。

## よし、早速やってみよう
ディレクトリ作って、そこに移動
```powershell
mkdir practice-pyxel && cd $_
```

ドキュメントに従い、Macなのでbrewでインストールして色々する
```powershell
brew install pipx
pipx ensurepath
pipx install pyxel
```

実際のサンプルから実際のコードや色々を確認するのは大事なのでサンプルを持ってくる

```powershell
pyxel copy_examples
```

実際に動くか確認
```powershell
cd pyxel_examples
pyxel run 01_hello_pyxel.py
pyxel play 30sec_of_daylight.pyxapp
```
helloが出て、ゲームが始まった。ウィンドウを閉じると終了されるっぽい。

## 自分で描いたファイルを実行してみる
ChatGPTに聞くとPythonではmain.pyというファイルが一般的らしいのでファイルを作る。
```powershell
# ルートに戻る
cd ../

touch main.py
```

ドキュメントのコードを貼り付けて実行してみる
```powershell
python3 main.py

# import pyxelしてるけど"pyxel"が見つからないよエラー
```

npm installみたいに、パッケージをインストールしないで使おうとしている状態か、調べるとpipというのを使用するらいいのでインストールしてみる。

```powershell
pip install pyxel

# command not found: pip | pipが見つからないよエラー
```

でも調べるとpython3には標準で入っているっぽい？
意味がないけど標準で入っているなら念の為もう一回

```powershell
pip install pyxel

# command not found: pip | pipが見つからないよエラー
```

https://zenn.dev/shimouta/articles/c81de05cafe6d2

クリティカルな記事を書いてくれている方がいましたので、倣って実行してみます。

```powershell
python3 -m pip install pyxel

# インストールできた
# でもパス通ってないよ警告
```

```powershell
nano ~/.zshrc
```
開かれるエディターの一番下に↓をコピペ（警告文に出ていたPath）
`export PATH="$HOME/Library/Python/3.9/bin:$PATH"`

1. controll + X で抜ける
2. YESで進む
3. Enterで閉じる

pathを通す
```powershell
source ~/.zshrc
```

再度自分のファイルを実行してみる

```powershell
python3 main.py
```
無事、四角が出ている画面が表示されましたー！これで準備完了なはず

## 一旦どうやって表示されているのか確認してみる

```py
# pyxelをimportする
import pyxel

# initでwindowサイズを決定する
pyxel.init(160, 120)

# なんかQを押すと起きるっぽい
# Qを押したら閉じたので、pyxel.quit()はwindow閉じるやつっぽい
# updateという名前からして、定期的に実行したい処理はここで実装しそう
def update():
    if pyxel.btnp(pyxel.KEY_Q):
        pyxel.quit()

# rectって書いてるのでここで四角書いてるっぽい drawだし
def draw():
    # 画面をまっさらにする（0は黒）
    pyxel.cls(0)
    # 値を変えてみたところ最後のは色、左から頂点座標っぽい
    pyxel.rect(10, 10, 20, 20, 11)

pyxel.run(update, draw)
```

実際のコードではクラスでラップした方がいいらしいので下記に書き換える。

```py
import pyxel

class App:
    # 上からの流れ的に多分最初に実行される（initだし…
    # ＿_とかついてるので多分VueのonMounted,Reactの空配列useEffect的な決まった記法かな
    # フロントエンドの感覚とは違うだろうけど
    def __init__(self):
        pyxel.init(160, 120)
        self.x = 0
        pyxel.run(self.update, self.draw)

    def update(self):
        # 余りを計算しているっぽいが、self.xが増加する処理が見えないので謎
        # updateというくらいなので、ある程度の間隔で定期実行されるのかな
        # 実行したら動いたのでそういうことっぽい
        self.x = (self.x + 1) % pyxel.width

    def draw(self):
        pyxel.cls(0)
        # drawは初期状態の表示に使用してupdateで変更されるself.xの値で移動しているっぽい
        pyxel.rect(self.x, 0, 8, 8, 9)

App()
```

実際に開発するときは`python file_name`ではなく、pyxel側で用意してくれている`pyxel watch WATCH_DIR Pythonスクリプトファイル`を使用して開発したらとても便利そう（使ったことはない）

## Pyxel Editorでマップやドット絵を作成していく

ドキュメント（README）の「リソースの作成方法」にあたる箇所になります。最初はPyxel Editorとはなんぞやだったのですが、ゲームに使用するドット絵や音楽が作れる場所っぽいので作成していきます。

## 現段階で何となくこういう流れかなと思ったPyxelでのゲームの作り方

1. Pyxel Editorでアイテムをドット絵で作る
2. Pyxel Editorでアイテムをマップに配置していく
3. Pyxel Editorで作成したマップに対して、キャラクターの動きをコードに書いていく

違う可能性もありますが一旦この認識で実装を進めていきます。

## Pyxel Editorを起動してみよう

```powershell
pyxel edit
```
ファイル名を指定しなければ`my_resource.pyxres`というファイル名になるらしいので、こだわりも無いためそのまま実行

おお！READMEに書いてあった画面が立ち上がった

![スクリーンショット 2024-11-20 13.46.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3734276/29f96641-cf15-c67d-deea-4ee75fcc86db.png)

あれ、ドット絵難しいぞ…
なんかいい方法ないのかな

![スクリーンショット 2024-11-20 13.52.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3734276/9c6fd3c4-ab78-62ca-f5ac-d1434cbbecee.png)

あまりにも自分にセンスがなかったため、サンプルを見にいきます

## サンプルを見学してみる

pyxel_examplesディレクトリに移動
```powershell
cd pyxel_examples
```

鳥さんが横スクロールで移動していくやつがイメージに近かったため、そのサンプルを見学。

| ファイル名          | 説明                         | デモURL | コード |
|-------------------|----------------------------|------|-------|
| 10_platformer.py   | マップのある横スクロールアクションゲーム | デモ  | コード  |

10番のサンプルのソースを読んでみると、platformer.pyxresを読み込んでいたため確認してみる。

```powershell
pyxel edit assets/platformer.pyxres
```

![スクリーンショット 2024-11-20 14.07.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3734276/87ef6002-5eb0-a37a-3632-dd6879c871dd.png)

おー色んな素材が置いてある！
左上の四角が四つ集まっているアイコンを押すとマップにアイテムを自分で設置して遊ぶことが出来たので先程の作成手順の仮説はあっていそう。

ただ、ドット絵は自分のセンスでは無理だと思うので一旦フリー素材探しの旅に出かけてきます。🏃💨

**30minutes later ...**

>8 × 8の画像じゃないと上手く読み込めないっぽい？
ダウンロードしても500 × 720とかになるので上手くいかない？
原因はよくわかっていませんが、いい感じに読み込めなかったです

ということで、無理でしたので自分で作ります。

## 適当に自分で作ってみた
**一旦適当に作ってみた**

ドット絵初心者にしては結構いい感じにできたのでは？

![スクリーンショット 2024-11-20 17.08.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3734276/3a52e3ea-c95a-c328-b533-b58f18afc5de.png)

![スクリーンショット 2024-11-20 17.08.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3734276/de23bbbd-53bf-03e1-91be-0afdbe5b5efc.png)

この状態で先程のドキュメントのサンプルコードで立ち上げても当然読み込まれないので一度load関数を使用して、作ったファイルを読み込んでみます。

```python
import pyxel

class App:
    def __init__(self):
        pyxel.init(160, 120)
        pyxel.load('my_resource.pyxres')
        self.x = 0
        pyxel.run(self.update, self.draw)

    def update(self):
        self.x = (self.x + 1) % pyxel.width

    def draw(self):
        pyxel.cls(0)
        pyxel.rect(self.x, 0, 8, 8, 9)

App()

```

これでも読み込まれないので、一度ドキュメントを見返します。

```md
🔸blt(x, y, img, u, v, w, h, [colkey], [rotate], [scale])

イメージバンクimg(0-2) の (u, v) からサイズ (w, h) の領域を (x, y) にコピーします。
w、hそれぞれに負の値を設定すると水平、垂直方向に反転します。
colkeyに色を指定すると透明色として扱われます。
rotate(度:Degree)、scale(1.0=100%)、またはその両方を指定すると対応する変換が適用されます。
```

ここが該当していそうなので、サンプルコードからImageBankの記述を使用していそうな箇所を探して今の自分のImageBankに調整して表示されるか確認してみます。

```py
def draw(self):
  # キャラクター画像の描画
  pyxel.blt(self.x, self.y, 0, 8, 8, 16, 16, 0)  # images[0] のキャラクター画像
```

ここがぽいので先程のコードに差し込んでみます。

```python
import pyxel

class App:
    def __init__(self):
        pyxel.init(160, 120)
        pyxel.load('my_resource.pyxres')
        self.x = 0
        pyxel.run(self.update, self.draw)

    def update(self):
        self.x = (self.x + 1) % pyxel.width

    def draw(self):
        pyxel.cls(0)
        pyxel.blt(1, 1, 0, 8, 8, 16, 16, 0)

App()

```

![スクリーンショット 2024-11-20 17.44.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3734276/95a0a652-b463-44ee-5962-b4c552c7b9e4.png)

x,yに1をそれぞれ指定しているので、左上に表示されていますが正しく表示されていそうです。

```py
pyxel.blt(1, 1, 0, 8, 0, 8, 8, 0)
```
上のコードに変更したらお化けが表示されたので問題なさそうですね

## タイルマップエディタで作成したMapを表示してみる

まずどうやるのか分からないので、サンプルとドキュメントを見比べてっぽいところを探します

ドキュメントで`bltm`が怪しかったので、サンプルコードで検索をかけてみると、Appクラスのdrawメソッドがそのまま初期化して、タイルマップ反映して、プレイヤーと敵を置くという処理になっていたので、playerクラス作成前のためそれ以外を記述してみます。

```py:10_platformer.py
def draw(self):
    pyxel.cls(0)

    # Draw level
    pyxel.camera()
    pyxel.bltm(0, 0, 0, (scroll_x // 4) % 128, 128, 128, 128)
    pyxel.bltm(0, 0, 0, scroll_x, 0, 128, 128, TRANSPARENT_COLOR)

    # Draw characters
    pyxel.camera(scroll_x, 0)
    player.draw()
    for enemy in enemies:
        enemy.draw()
```

pyxel.bltm(0, 0, 0, scroll_x, 0, 128, 128, TRANSPARENT_COLOR)のままの記述だとTRANSPARENT_COLORも設定されておらずにスクロールの設定もしていないため表示されないので、タイルマップを確認しつつ調整します。
```py:main.py
def draw(self):
    pyxel.cls(0)

    pyxel.bltm(0, 0, 0, 0, 0, 128, 128)
```

![スクリーンショット 2024-11-20 19.40.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3734276/82478359-8b56-f260-df34-dc25a9241277.png)

うれしー！正直ここに30分かかりました…(謎)
お化けかわいい、適当ドット絵主人公も愛着湧いてきている

## sampleのコードを見ながらplayerクラスを作成してみる

### 私がplayerクラスに求めること
- 十字キーで移動ができること
- SPACEでジャンプすること
- 自然な感じで落下すること

後から追加
- 地面に立てること（設定しないと落ちていってしまう）
- 将来的に左右に慣性が欲しい（ここでは一旦保留）

とりあえず、ドキュメントを見てるとボタンを押すとtrueを返してくれる部分が見つかりました。

```
- btnp(key, [hold], [repeat])
そのフレームにkeyが押されたらTrue、押されなければFalseを返します。
holdとrepeatを指定すると、holdフレーム以上ボタンを
押し続けた時にrepeatフレーム間隔でTrueが返ります。
```

```py:main.py
import pyxel

class Player:
    # Playerの初期化、x,yの引数で初期位置を与える
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.width = 8
        self.height = 8
        # 移動速度やそれぞれの値
        self.speed = 2
        self.jump_power = 6
        self.gravity = 0.5
        self.y_velocity = 0
        # 名前的に空中か地面かの判定
        self.is_jumping = False
        self.on_ground = False

    def update(self):
        # 横方向の移動ロジック（直接数字にしがちだったけどマジックナンバーになるからちゃんと変数にした方がいい）
        if pyxel.btn(pyxel.KEY_LEFT):
            self.x -= self.speed
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.x += self.speed

        # ジャンプのロジック（直接じゃなくてvelocityだから速度の値を変えることで重力というか縦方向の落ちていく動作を再現するのかー）
        if pyxel.btnp(pyxel.KEY_SPACE) and self.on_ground:
            self.y_velocity = -self.jump_power
            self.is_jumping = True
            self.on_ground = False

        # 滑らかに縦に落ちる処理（重力）
        self.y_velocity += self.gravity
        self.y += self.y_velocity

        # 地面の高さを指定して、それ以下になると縦方向の落下を止める。＝　地面に立つ的なことか
        ground_level = 120
        if self.y + self.height > ground_level:
            self.y = ground_level - self.height
            self.y_velocity = 0
            self.is_jumping = False
            self.on_ground = True

        # 画面外に出ないようにする処理？
        self.x = max(0, min(self.x, pyxel.width - self.width))

    def draw(self):
        pyxel.blt(self.x, self.y, 0, 8, 8, self.width, self.height, 0)

class App:
    def __init__(self):
        pyxel.init(160, 128)
        pyxel.load('my_resource.pyxres')
        # x,yの値を与えて初期位置を設定(px)
        self.player = Player(10, 100)
        pyxel.run(self.update, self.draw)

    def update(self):
        self.player.update()

    def draw(self):
        pyxel.cls(0)
        # タイルマップの範囲指定
        pyxel.bltm(0, 0, 0, 0, 0, 128, 128)
        self.player.draw()

App()
```
サンプルコードを見てあまり理解できていないものの一旦流用してみるとキャラクターが動きました！


## 一旦AIに処理は書いてもらっちゃおう！

こちらが指定したオブジェクトに当たり判定を追加、特定のX座標時に到達したらゴール、敵は左右に動く、敵に触れたらゲームオーバーなどを指定してコードを書いてもらいました。

<details><summary>長いので省略</summary>


```python
import pyxel

# 今回だとお化けに関するクラス
class Enemy:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.dx = 1
        self.direction = 1
        self.is_alive = True
        self.start_x = x
        # 1マス８pxなので3マス範囲で移動するための値
        self.move_range = 24
        self.initial_direction = 1

    def update(self):
        # 右に移動
        self.x += self.dx

        # 初期位置から3マス分の範囲で往復
        # 初期位置からxの値が範囲を超えたら座標を差し引き
        if self.x >= self.start_x + self.move_range:
            self.x = self.start_x + self.move_range
            self.dx *= -1
            self.direction *= -1
        elif self.x <= self.start_x:
            self.x = self.start_x
            self.dx *= -1
            self.direction *= -1

    # 左右の向いている方向でお化けの向く向きも変える
    def draw(self):
        if self.direction == 1:
            pyxel.blt(self.x, self.y, 0, 16, 8, 8, 8)
        else:
            pyxel.blt(self.x, self.y, 0, 8, 0, 8, 8)


class Player:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.dx = 0
        self.dy = 0
        self.is_falling = False
        self.on_ground = False
        self.direction = 1
        
        # ジャンプ関係の値
        self.jump_count = 0
        # 二段ジャンプさせたい
        self.max_jumps = 2
        # ジャンプの強さ
        self.jump_strength = -5.5
        # コードを読んだ感じHoldでジャンプの高さが変わるっぽいので最小値？
        self.jump_cut_off = -2.0
        self.gravity = 0.35
        self.max_fall_speed = 6
        
        self.is_dashing = False
        self.dash_cooldown = 0
        self.dash_duration = 0
        self.acceleration = 0.5
        self.max_speed = 3
        self.friction = 0.85
        # ゲームオーバーかどうかの判定値
        self.is_alive = True

    def check_tile_collision(self, x, y):
        # タイルのサイズ（8x8）を考慮して、プレイヤーの四隅のタイル座標をチェック
        tile_coords = [
            (int(x / 8), int(y / 8)),  # 左上
            (int((x + 7) / 8), int(y / 8)),  # 右上
            (int(x / 8), int((y + 7) / 8)),  # 左下
            (int((x + 7) / 8), int((y + 7) / 8))  # 右下
        ]
        
        for tile_x, tile_y in tile_coords:
            # タイルマップの範囲内かチェック
            if 0 <= tile_x < 45 and 0 <= tile_y < 16:
                tile = pyxel.tilemap(0).pget(tile_x, tile_y)
                # 地面として扱うタイル（X: 0-1, Y: 1-2のタイルと0-1, Y: 2-3のタイル）をチェック
                if (tile[0] == 0 and tile[1] == 1) or (tile[0] == 0 and tile[1] == 2):  # 地面とオブジェクトのタイル
                    return True, tile_y * 8  # 衝突したタイルのY座標も返す
        return False, 0

    def update(self):
        # ダッシュのクールダウン
        if self.dash_cooldown > 0:
            self.dash_cooldown -= 1
        if self.dash_duration > 0:
            self.dash_duration -= 1
        else:
            self.is_dashing = False

        # 左右移動
        if pyxel.btn(pyxel.KEY_LEFT):
            self.dx -= self.acceleration
            self.direction = -1
        elif pyxel.btn(pyxel.KEY_RIGHT):
            self.dx += self.acceleration
            self.direction = 1
        else:
            self.dx *= self.friction

        # 速度制限
        if not self.is_dashing:
            self.dx = max(-self.max_speed, min(self.max_speed, self.dx))

        # ダッシュ
        if pyxel.btnp(pyxel.KEY_SHIFT) and self.dash_cooldown <= 0:
            self.is_dashing = True
            self.dash_duration = 10
            self.dash_cooldown = 30
            self.dx = self.direction * self.max_speed * 2

        # ジャンプ
        if pyxel.btnp(pyxel.KEY_SPACE):
            if self.on_ground:
                self.dy = self.jump_strength
                self.is_falling = True
                self.on_ground = False
                self.jump_count = 1
            elif self.jump_count < self.max_jumps:
                self.dy = self.jump_strength
                self.jump_count += 1
        
        # ジャンプカットオフ（ジャンプボタンを離したときの処理）
        if not pyxel.btn(pyxel.KEY_SPACE) and self.dy < self.jump_cut_off:
            self.dy = self.jump_cut_off

        # 重力の適用
        if not self.on_ground:
            self.dy += self.gravity
            self.dy = min(self.dy, self.max_fall_speed)  # 落下速度の制限

        # 座標の更新（X方向）
        next_x = self.x + self.dx
        next_y = self.y

        # X方向の当たり判定
        has_collision, _ = self.check_tile_collision(next_x, self.y)
        if not has_collision:
            self.x = next_x
        else:
            # 壁にぶつかった場合
            self.dx = 0

        # Y方向の更新と当たり判定
        next_y = self.y + self.dy
        
        has_collision, ground_y = self.check_tile_collision(self.x, next_y)
        if not has_collision:
            self.y = next_y
            # 下向きの移動中は地面との接触をチェック
            if self.dy > 0:
                # 次のフレームでの地面チェック
                next_has_collision, next_ground_y = self.check_tile_collision(self.x, next_y + 1)
                if next_has_collision:
                    # 地面との接触が近い場合、スナップする
                    self.y = next_ground_y - 8
                    self.dy = 0
                    self.is_falling = False
                    self.on_ground = True
                    self.jump_count = 0
                else:
                    self.on_ground = False
                    self.is_falling = True
            else:
                self.on_ground = False
                self.is_falling = True
        else:
            # 地面に着地した場合
            if self.dy > 0:  # 下向きの移動の場合
                # タイルの上端に正確に位置を合わせる
                self.y = ground_y - 8
                self.dy = 0
                self.is_falling = False
                self.on_ground = True
                self.jump_count = 0
            else:  # 上向きの移動の場合（天井にぶつかった）
                self.y = ground_y + 8
                self.dy = 0

        # マップ全体での移動制限（45タイル=360px）
        self.x = max(0, min(self.x, 360 - 8))  # マップの幅に合わせて制限

    def check_collision(self, enemy):
        # 敵との当たり判定
        if (abs(self.x - enemy.x) < 8 and
            abs(self.y - enemy.y) < 8):
            self.is_alive = False
            return True
        return False

    def draw(self):
        # プレイヤーの向きに応じて描画
        if self.direction == 1:
            pyxel.blt(self.x, self.y, 0, 8, 8, 8, 8, 0)
        else:
            pyxel.blt(self.x, self.y, 0, 8, 8, -8, 8, 0)  # 左向きの場合は反転


class App:
    def __init__(self):
        pyxel.init(160, 128)
        pyxel.load('my_resource.pyxres')
        self.init_game()
        pyxel.run(self.update, self.draw)

    def spawn_enemies(self):
        enemies = []
        # tilemapをスキャンして敵を配置
        for y in range(16):
            for x in range(45):  # 45タイル分スキャン
                tile = pyxel.tilemap(0).pget(x, y)
                if tile == (1, 0):  # 敵のタイル
                    enemies.append(Enemy(x * 8, y * 8))
                    # 敵の位置のタイルを透明なタイルに変更
                    pyxel.tilemap(0).pset(x, y, (0, 0))
        return enemies

    def init_game(self):
        # タイルマップを初期状態に戻す
        pyxel.load('my_resource.pyxres')
        self.player = Player(0, 0)
        self.enemies = self.spawn_enemies()  # tilemapから敵を生成
        self.game_over = False
        self.game_clear = False  # ゲームクリアフラグを追加
        self.camera_x = 0  # カメラのX位置を追加

    def update(self):
        if not self.game_over and not self.game_clear:
            self.player.update()
            
            # ゲームクリア判定を追加（312px = 39タイル目）
            if self.player.x >= 312:
                self.game_clear = True
            
            # カメラの位置をプレイヤーに追従（画面中央に表示）
            target_camera_x = self.player.x - pyxel.width // 2
            # カメラが0より左に行かないようにする（45タイル=360px）
            self.camera_x = max(0, min(target_camera_x, 360 - pyxel.width))
            
            # 敵の更新と当たり判定
            for enemy in self.enemies:
                # 画面から少し余裕を持った範囲の敵だけ更新する
                if -64 <= enemy.x - self.camera_x <= pyxel.width + 64:
                    enemy.update()
                    if self.player.check_collision(enemy):
                        self.game_over = True
        else:
            # リスタート
            if pyxel.btnp(pyxel.KEY_R):
                self.init_game()

    def draw(self):
        pyxel.cls(0)
        # カメラ位置を考慮してマップを描画（45タイル=360px）
        pyxel.bltm(0, 0, 0, self.camera_x, 0, 360, 128)

        if not self.game_over and not self.game_clear:
            # プレイヤーの描画（実際の位置で描画）
            real_x = self.player.x - self.camera_x
            pyxel.blt(real_x, self.player.y, 0, 8, 8, 8 if self.player.direction == 1 else -8, 8, 0)

            # 敵の描画（実際の位置で描画）
            for enemy in self.enemies:
                real_x = enemy.x - self.camera_x
                # 画面内の敵だけ描画
                if -8 <= real_x <= pyxel.width + 8:
                    if enemy.direction == 1:
                        pyxel.blt(real_x, enemy.y, 0, 16, 8, 8, 8)
                    else:
                        pyxel.blt(real_x, enemy.y, 0, 8, 0, 8, 8)
        elif self.game_clear:
            # ゲームクリア画面
            pyxel.text(60, 64, "GAME CLEAR!", 11)
            pyxel.text(45, 74, "PRESS R TO RESTART", 8)
        else:
            # ゲームオーバー画面
            pyxel.text(60, 64, "GAME OVER", 8)
            pyxel.text(45, 74, "PRESS R TO RESTART", 8)


App()
```

</details>

うわー、すごく長い。必要な機能をそれぞれ分解していけば自分が作りたいクリスマスっぽいゲームを作れそう。

とりあえず、サンプルが完成したので公開

### お化けに当たらないように、ゴールを目指せ！
```
移動：十字　ジャンプ：SPACE 　ダッシュ：SHIFT
```
https://kitao.github.io/pyxel/wasm/launcher/?run=ojagao.sample-pyxel.game.main 

![スクリーンショット 2024-11-28 19.32.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3734276/0d5c999e-1fac-137b-5b1b-50e3b3ae5fad.png)

## クリスマスっぽい装飾を加えていきます

上の方でも一度書いたドット絵を描くPyxel Editorを使用してクリスマスっぽくしていきます！

- キャラクターの慣性はあった方がリッチになるかと思いきや個人的にマイナス評価だったため削除
- ダッシュ機能もいらないので削除
- 雪降ってた方がいい感じになりそうなので雪が降る処理を追加

![スクリーンショット 2024-12-12 19.03.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3734276/3e232e5d-cc41-3ab0-690f-f50d3a60e5b6.png)


## 改めて完成品

🎊〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜🎊

https://kitao.github.io/pyxel/wasm/launcher/?run=ojagao.sample-pyxel.christmas-game.main

🎊〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜🎊

![c-game.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3734276/9173c394-d9e9-b245-edab-1193ac472ce6.gif)
↑ 動きが重たいもっさりGIF

## 作ってみた感想

完全初心者が数時間で作成できたので、とても作りやすい環境だなと感じました！それと同時に作り込もうと思ったら、とても作り込めそうな雰囲気を感じましたので今後も趣味の範囲で作っていくかもしれません。

自分のデザインセンスではクリスマス感を出し切れず、空にサンタさんを置いたりとかリース設置したりもっと赤を追加したかったのですが不完全燃焼感もあるので来年のクリスマスにはもっと凝ったゲームを作ったり作らなかったりとか…

最後まで読んでいただきありがとうございました

良いクリスマスを！🎄

## 参考記事（作ろうと思うきっかけになった記事）

https://qiita.com/kitao/items/eae53dd47c663b497352

https://qiita.com/shiromofufactory/items/7f5c44b55343bf90651b