このドキュメントはエンジン開発初期段階に作られたものです。機能変更がされ、コードが正常に動作しない場合があります。
このページでは実際にTHEngine37を使ってブロック崩しゲームを作ります。
まず最初に、ゲームの基礎を築きましょう。
こちらのGitHubページに接続し 、右上の"Code"から下にある"Download Zip"ボタンを押してダウンロードします。
ダウンロードが完了したら解凍して、適当な位置に配置しましょう。Pygame等依存ライブラリがあるのでダウンロードをしてください(注意点にあるライブラリ)。これで、THEngineのダウンロードは完了です。
(*注意点)THEngine37は以下の環境でのみ動作確認が取れています。各バージョンを以下のものに合わせるのをお勧めします。
Python: 3.12.6
pygame: 2.6.1
numpy: 2.0.2
ウィンドウの大きさやタイトル、FPS等の設定をします。
設定方法は至って簡単です。以下のSettings.iniファイルを書き換えるだけです。
Settings.iniはconfigsフォルダ内に存在し、開くと画像のように設定が記述されています。詳しくはドキュメント を参照してください。
WIDTH、HEIGHT、CAPTIONを自由に設定しましょう。
最後のSCENEの項目は"SampleScene"と書かれている部分を削除して新たに"Game"と書いてください。
シーンはゲームが動作するうえで極めて重要な要素です。シーンは以下のように追加します。
ScenesフォルダのSampleScene.jsonを削除してください。
新たにGame.jsonという名前のファイルを追加してください。
Game.jsonを画像のように編集します。
これでゲームの基礎が出来ました。動作確認をしましょう。
main.pyを実行することで、ゲームが起動します。
画像のような黒い画面が表示されたら成功です。
1ではゲームの基礎を築きました。次はプレイヤーをゲームに追加しましょう。
ここで、目的を再確認しましょう。我々の目標はブロック崩しゲームを作ることです。というわけで、プレイヤーはただの白い棒とします。
幸運にも白い四角形の画像がTHEngine37には標準で用意されています。これを使って白い棒を追加します。
またGame.jsonファイルを開き、MainCameraに続き次の内容を記述してください。
"Player": {
"tag": "pl",
"layer": 0,
"id": 1,
"components": {
"Transform": {
"_type": "Transform",
"value": {
"x": 0,
"y": 0,
"width": 300,
"height": 50,
"rotate": 0
}
},
"Sprite": {
"_type": "Sprite",
"value": {
"src": "./Images/Squere.png"
}
}
}
}
ここで少し解説をします。Componentsの中にTransformとSpriteの存在が確認できます。Componentsは一つのゲームオブジェクト(今回でいうと"Player")を作る部品のようなものと思ってください。ゲームオブジェクトを"スマホ"に例えるならばコンポーネントは"バッテリー"や"スクリーン"に当たります。これらなしにはスマホは起動しませんよね?
Transformはプレイヤーの位置、大きさ、角度を指定するためのコンポーネントです。
Spriteは画像を画面に出すためのコンポーネントです。画像の位置はTransformで指定した位置となり、大きさもそれに指定されたものとなります。
プレイヤーが画面に出るようになったので、動かせるようにしましょう。
画像のように、新たにcomponentsというフォルダを作り、その中にPlayer.pyというファイルを作りましょう。
作成出来たら以下のプログラムを書いてください。
from config import GameManager, BaseCPN, Transform
import pygame as pg
class PlayerMove(BaseCPN):
def __init__(self):
super().__init__()
self.transform: Transform = None
self.speed: float = 3
self.gm: GameManager = GameManager()
def Start(self):
self.transform = self.gameobject.GetComponent(Transform)
def Update(self):
key = pg.key.get_pressed()
if key[pg.K_RIGHT]:
self.transform.x += self.speed * self.gm.deltaTime
if key[pg.K_LEFT]:
self.transform.x -= self.speed * self.gm.deltaTime
PlayerMoveはコンポーネントとして書きました。
Start()関数はシーン開始の最初だけ呼ばれ、Update()関数はゲーム中に常に呼ばれます。
transformでは、同じゲームオブジェクト(プレイヤー)につけられているTransformコンポーネントを取得するため、Start()関数内でGetComponent(Transform)をしています。いかなる場合でも、コンポーネントの取得は__init__()で行ってはいけませんので注意が必要です。
SceneLoader.pyを開いて、画像のように編集してください。SceneLoaderでは先ほどのコンポーネントをゲームエンジン内で使えるようにします。
まずは先ほどのPlayerMoveを書いたコードをインポートしましょう。
インポートが出来たら、SceneLoaderクラスのobject_hook()関数内に青色に塗られた部分を新たに追加してください。
これでゲームエンジンでPlayerMoveクラスが使えるようになりました。次はゲームオブジェクトにコンポーネントを追加しましょう。
シーンファイル(Game.json)を開いて、画像のように書き加えます。componentsにPlayerMoveを加えることで追加可能です。
コンポーネントは次の二つの項があります。
_type
ゲームエンジンでコンポーネントを認識するための名前
objecthook関数に書き加えた cls == "PlayerMove" の部分です
value
コンポーネントに与える変数です。これはコンポーネントによります。
与える変数がない場合もこれを書く必要があります。
先ほど同様、main.pyを実行して動作確認しましょう。
すると、画面に白い棒が追加され、左右の矢印キーで移動ができるはずです。遅く感じる場合はPlayerMoveのspeedの値を変化させてみましょう。
プレイヤーが追加できたので、ボールを追加しましょう。componentsフォルダを開いて、Ball.pyというファイルを新規作成します。ファイルには以下のコードを記述してください。
from config import GameManager, BaseCPN, Transform, HitBox, GameObject
import numpy as np
class BallMove(BaseCPN):
def __init__(self):
super().__init__()
self.transform: Transform = None
self.speed: float = 4
self.gm: GameManager = GameManager()
self.vector: np.ndarray = np.array([1, 0.6]) # 進行方向
self.hitbox: HitBox = None # ボール自身のヒットボックス
self.walls_objects: list[GameObject] = [] # 壁のヒットボックス(左右)
self.walls_x: list[HitBox] = [] # 壁のコンポーネント(左右)
self.wall_y: HitBox = None # 壁のヒットボックス(上)
self.player: HitBox = None # プレイヤーのヒットボックス
def Start(self):
self.transform = self.gameobject.GetComponent(Transform)
self.hitbox = self.gameobject.GetComponent(HitBox)
self.walls_objects = self.gameobject.scene.GetObjectsWithTag("wall_x")
self.walls_x = [obj.GetComponent(HitBox) for obj in self.walls_objects]
self.wall_y = self.gameobject.scene.GetObjectRequest(2).GetComponent(HitBox)
self.player = self.gameobject.scene.GetObjectRequest(1).GetComponent(HitBox)
def Update(self):
for wx in self.walls_x:
if self.hitbox.isCollideVector(wx, self.vector[0], self.vector[1]): # 左右の壁との衝突判定
self.vector[0] *= -1 # 横の進行方向を反転させる
if self.hitbox.isCollideVector(self.wall_y, self.vector[0], self.vector[1]) or self.hitbox.isCollideVector(self.player, self.vector[0], self.vector[1]): # 上の壁またはプレイヤーとの衝突判定
self.vector[1] *= -1 # 縦の進行方向を反転させる
self.transform.x += self.vector[0]
self.transform.y += self.vector[1]
ヒットボックス用の壁は後で、シーンファイルに記述します。
コード中の self.gameobject.scene.GetObjectRequest() はシーンからGetObjectRequestに与えたIDのゲームオブジェクトを取得しています。
シーンファイルに書くゲームオブジェクトのIDが重複すると競合が起こり想定するゲームオブジェクトを取得できません。そのため、ゲームオブジェクトのIDはただ一つ、そのオブジェクトのもつ固有のIDである必要があります。また、IDは数字で表現します。
Update()関数内ではボールが左右の壁、上の壁、プレイヤーと衝突したときに進行方向を反転させています。簡単に言えば、衝突時に跳ね返る処理を実装しています。
進行方向はvectorという変数で表しています。その名の通りベクトルというのは数学的な概念です。ゲームではこの概念をよく扱うのでよく理解してください。
では、シーンファイルに"壁"を追加しましょう。コードに以下の内容を追加してください。
"Wall_left": {
"tag": "wall_x",
"layer": 0,
"id": 2,
"components": {
"Transform": {
"_type": "Transform",
"value": {
"x": -8,
"y": 0,
"width": 50,
"height": 720,
"rotate": 0
}
},
"HitBox": {
"_type": "HitBox",
"value": {
"w": 50,
"h": 720
}
}
}
},
"Wall_right": {
"tag": "wall_x",
"layer": 0,
"id": 3,
"components": {
"Transform": {
"_type": "Transform",
"value": {
"x": 8,
"y": 0,
"width": 50,
"height": 720,
"rotate": 0
}
},
"HitBox": {
"_type": "HitBox",
"value": {
"w": 50,
"h": 720
}
}
}
},
"Wall_up": {
"tag": "",
"layer": 0,
"id": 4,
"components": {
"Transform": {
"_type": "Transform",
"value": {
"x": 0,
"y": -4,
"width": 2000,
"height": 50,
"rotate": 0
}
},
"HitBox": {
"_type": "HitBox",
"value": {
"w": 2000,
"h": 50
}
}
}
}
これでヒットボックスが追加されました。ヒットボックスコンポーネントは"w"と"h"を与えなければなりません。それぞれ幅と高さを表します。ヒットボックスの大きさを指定しましょう。
追加で、PlayerオブジェクトにもHitBoxを追加しましょう。上のコードを参考にPlayerオブジェクトのcomponentsにHitBoxコンポーネントを追加しましょう。
次に、ボールのオブジェクトを追加しましょう。次のコードをシーンファイルに追加してください。
"Ball": {
"tag": "ball",
"layer": 0,
"id": 5,
"components": {
"Transform": {
"_type": "Transform",
"value": {
"x": 0,
"y": -2,
"width": 50,
"height": 50,
"rotate": 0
}
},
"Sprite": {
"_type": "Sprite",
"value": {
"src": "./Images/Squere.png"
}
},
"HitBox": {
"_type": "HitBox",
"value": {
"w": 50,
"h": 50
}
},
"BallMove": {
"_type": "BallMove",
"value": {}
}
}
}
シーンにオブジェクトを配置することができました。次は、BallMoveコンポーネントをSceneLoader.pyのobject_hook()関数に登録しましょう。以下のように付け足してください。
(*注意) インポートは忘れずにしましょう。
これでボールをゲームに追加できました。動作確認をしてみましょう。ボールといいつつ四角形ですが、しっかりと壁とプレイヤーにぶつかって跳ね返ります。
プレイヤー、ボールと追加できたため、次は崩すブロックを追加しましょう。通常のブロック崩しでは、大量のブロックが存在します。
大量のブロックをシーンに追加するにはどうすればよいでしょうか?
ひとつひとつブロックを追加していては、あなたの寿命は尽きてしまうでしょう(非効率ということです)。そこで、ブロックジェネレーターを作りましょう。具体的には、下の図のように、ゲーム開始時にブロックを何個も生成するためのオブジェクトを一つ作ります。
さて、ブロックジェネレーターを作るにあたっての指針をまとめましょう。
ブロックのオブジェクトを定義する
ゲームジェネレーターで定義したブロックオブジェクトを生成
ブロックオブジェクトはどのような形状をしていて、何のコンポーネントを持っていて・・・
ということをコードに落とし込んでいきます。それをクラスとして実装しましょう。
では、まず"BlockGenerator.py"というファイルをcomponentsフォルダの中に新規作成しましょう。そして、以下の内容を書き込みます。コンポーネントを作成します。
from config import GameManager, BaseCPN, Transform, HitBox, GameObject, Sprite
class Block(BaseCPN):
def __init__(self):
super().__init__()
self.transform: Transform = None
self.HitBox: HitBox = None
self.Ball: HitBox = None
def Start(self):
self.transform = self.gameobject.GetComponent(Transform)
self.HitBox = self.gameobject.GetComponent(HitBox)
self.Ball = self.gameobject.scene.GetObjectRequest(5).GetComponent(HitBox) # ボールのHitBox
def Update(self):
if self.HitBox.isCollide(self.Ball):
self.gameobject.scene.gameObjects.remove(self.gameobject) # ボールと衝突した場合、自身を世界から削除する
次の内容を新たに書き加えてください。
class BlockGenerator(BaseCPN):
def __init__(self):
super().__init__()
self.gm = GameManager()
def OnLoad(self, GameObject):
super().OnLoad(GameObject)
self.blocks = []
for b in range(-2, 3):
self.blocks.append(self.generate_one(b+2, -3, 100, 50, b+7+2, GameObject))
for bl in self.blocks:
self.gameobject.scene.gameObjects.append(bl)
def generate_one(self, x, y, w, h, id, go) -> GameObject:
trs = Transform()
trs.x = x
trs.y = y
trs.w = w
trs.h = h
cpms = [trs, Sprite("./Images/Squere.png"), HitBox(trs.w, trs.h), Block()]
obj = GameObject()
obj.components = [cpm for cpm in cpms]
obj.id = id
obj.tag = "block"
obj.OnLoad(go)
return obj
generate_one()関数でブロック生成の処理を書きます。そして、OnLoad()で生成したブロックを配置しています。
いつもの通り、シーンファイルへのオブジェクトの追加とSceneLoaderへBlockGeneratorコンポーネントの追加を行います。
シーンファイルへの追加:
"Generator": {
"tag": "ball",
"layer": 0,
"id": 6,
"components": {
"Transform": {
"_type": "Transform",
"value": {
"x": 0,
"y": -2,
"width": 50,
"height": 50,
"rotate": 0
}
},
"BlcokGenerator":{
"_type": "BlockGenerator",
"value": {}
}
}
}
SceneLoader.pyへのBlockGeneratorの追加:
(*注意) BlockGenerator.pyのインポートを忘れずに!!
elif cls == "BlockGenerator":
return BlockGenerator()
このコードをobject_hook()関数のIF文に追加
GIF画像のように、ブロックが生成され、ボールに当たるとブロックが消えるはずです。
ブロックはボールに当たると消えますが、ボールが反射しません。これでは、ゲームは簡単すぎます。ということで、ボールがブロックに当たると反射するようコードを修正しましょう。
以下のように修正してください:
BlockGenerator.pyのBlockコンポーネント、Update()関数の処理を削除し、コードを以下のようにします。
class Block(BaseCPN):
def __init__(self):
・・・・・・省略
def Update(): ・・・・・・削除
def Destroy(self): ・・・追加
self.gameobject.scene.gameObjects.remove(self.gameobject) # 自身を世界から削除する
Ball.pyのBallMoveコンポーネント、collide_blocks()関数と縦方向の衝突判定の処理を以下のように変更します。緑部分が変更・追加点です。
from config import GameManager, BaseCPN, Transform, HitBox, GameObject
from conponents.BlockGenerator import Block
import numpy as np
class BallMove(BaseCPN):
def __init__(self):
super().__init__()
self.transform: Transform = None
self.speed: float = 4
self.gm: GameManager = GameManager()
self.vector: np.ndarray = np.array([1, 0.6]) # 進行方向
self.hitbox: HitBox = None # ボール自身のヒットボックス
self.walls_objects: list[GameObject] = [] # 壁のヒットボックス(左右)
self.walls_x: list[HitBox] = [] # 壁のコンポーネント(左右)
self.wall_y: HitBox = None # 壁のヒットボックス(上)
self.player: HitBox = None # プレイヤーのヒットボックス
self.bhb: HitBox = None
def Start(self):
self.transform = self.gameobject.GetComponent(Transform)
self.hitbox = self.gameobject.GetComponent(HitBox)
self.walls_objects = self.gameobject.scene.GetObjectsWithTag("wall_x")
self.walls_x = [obj.GetComponent(HitBox) for obj in self.walls_objects]
self.wall_y = self.gameobject.scene.GetObjectRequest(4).GetComponent(HitBox)
self.player = self.gameobject.scene.GetObjectRequest(1).GetComponent(HitBox)
self.bhb = [blockgameobj.GetComponent(HitBox) for blockgameobj in self.gameobject.scene.GetObjectsWithTag("block")]
def collide_blocks(self):
self.bhb = [blockgameobj.GetComponent(HitBox) for blockgameobj in self.gameobject.scene.GetObjectsWithTag("block")]
for b in self.bhb:
if self.hitbox.isCollideVector(b, self.vector[0], self.vector[1]):
b.gameobject.GetComponent(Block).Destroy()
return True
return False
def Update(self):
for wx in self.walls_x:
if self.hitbox.isCollideVector(wx, self.vector[0], self.vector[1]): # 左右の壁との衝突判定
self.vector[0] *= -1 # 横の進行方向を反転させる
if self.hitbox.isCollideVector(self.wall_y, self.vector[0], self.vector[1]) or self.hitbox.isCollideVector(self.player, self.vector[0], self.vector[1]) or self.collide_blocks(): # 上の壁またはプレイヤーとの衝突判定
self.vector[1] *= -1 # 縦の進行方向を反転させる
self.transform.x += self.vector[0] * self.speed * self.gm.deltaTime
self.transform.y += self.vector[1] * self.speed * self.gm.deltaTime
これで、ボールの反射が実装できました。
だいぶゲームらしくなってきましたが、肝心な事を忘れています。ゲーム―オーバーです。プレイヤーの危機感は現状ありません。ですので、ゲームオーバーを実装しましょう。
ボールが画面下に触れたかどうかを判定するため、当たり判定をシーンに追加しましょう。HitBoxを持ったゲームオブジェクトをいつも通り追加します。Game.jsonファイルを開いて以下の文を追加してください。
"DeathBox": {
"tag": "Death",
"layer": 0,
"id": 100,
"components": {
"Transform": {
"_type": "Transform",
"value": {
"x": 0,
"y": 4,
"width": 2000,
"height": 50,
"rotate": 0
}
},
"HitBox": {
"_type": "HitBox",
"value": {
"w": 2000,
"h": 50
}
}
}
}
ゲームオーバー後に「Game Over」と画面が切り替わり、表示されるようにしましょう。画面を切り替えるには現在のシーンを別のシーンに切り替える必要があります。
まずはゲームオーバーの当たり判定とシーン遷移の処理を作ります。
Ball.pyのBallMoveコンポーネントの__init__()内で次の変数を宣言し、
self.deathBox: HitBox = None
Start()内でゲームオブジェクトからを取得します。
self.deathBox = self.gameobject.scene.GetObjectRequest(100).GetComponent(HitBox)
実はこれは実に悪いコードで、デスボックスのIDは100番と、適当な数にしています。IDは連続的であるべきですが、今回はブロックの数が4個しかない為、連続的ではありません(連続的とは1,2,3,4,5,6・・・と続くことを言います)。現状ではエラーを起こしていませんが、このコードはエラーを起こしえます。例えば、ブロックや他のオブジェクトを94個ほどこわ得ると、100番のIDをもつオブジェクトが重複してしまいます。
SceneLoader.pyをインポートして、
import SceneLoader as sl
ボールがデスボックスに触れた時にシーン遷移をするようにしましょう。
Update()関数内に以下の内容を加えてください。
if self.hitbox.isCollide(self.deathBox):
sl.SceneLoader().load_scene("GameOver")
シーンのロードはSceneLoader().load_scene()で行います。load_scen()関数にはシーンの名前を入れます。まだシーンファイルを追加していませんが、遷移先のシーンはGameOverとします。シーン名はシーンファイルの名称です(.jsonは除く)。
次に、ゲームオーバー用のシーンファイルを追加します。Scenesフォルダを開いて、 GameOver.json を新規作成してください。
シーンには "Game Over" と画面に表示されるようにしましょう。以下のようにGameOver.jsonに書き込んでください。
{
"MainCamera": {
"tag": "",
"layer": 0,
"id": 0,
"components": {
"Transform": {
"_type": "Transform",
"value": {
"x": 0,
"y": 0,
"width": 1,
"height": 1,
"rotate": 0
}
},
"Camera": {
"_type": "Camera",
"value": {
}
}
}
},
"Text": {
"tag": "",
"layer": 0,
"id": 0,
"components": {
"Transform": {
"_type": "Transform",
"value": {
"x": 640,
"y": 360,
"width": 1,
"height": 1,
"rotate": 0
}
},
"Text": {
"_type": "Text",
"value": {
"text": "Game Over",
"size": 36,
"r": 255,
"g": 255,
"b": 255
}
}
}
}
}
Textコンポーネントについて、注意があります。Textコンポーネントをゲームオブジェクトに付与した場合、ゲームオブジェクトの座標系が変化します。通常、原点は画面の中心になりますが、この場合に限っては画面左上が原点になります。
これで、ブロック崩しゲームが完成しました。
しかし、このゲームには欠陥が多く残っています。例えば、ボールがブロックの側面に触れた時、y方向の速さのみ変化し、不自然な挙動に見えます。これは、ボールの衝突判定を4方向で行っていない為です。この問題を解決する方法はこのドキュメントでは紹介しませんが、意外とシンプルな方法で解決できます。こういった欠陥をプレイながら探し出し、解決していくと面白いかもしれません。
このドキュメントでは、ブロック崩し(?)の製作を通して、THEngine37のいくつかの機能を紹介しました。機能をさらに詳しく知りたい場合、このサイトのメニューから詳細な情報を得られるため、そちらを見てください。