はじめに
こんにちは。今回は命題に掲げている通りフレームワークを作った(ている)話をします。
初めての記事なので至らぬ点も多いと思いますが生暖かい目で見守りつつご覧ください。
Eagle for Siv3Dとは
Eagle for Siv3D(以下Eagle)は私が一年ほど前まで制作していたDxLibをラッピングした
フレームワークをSiv3D用に移植、改善したものです。
UnityC#スクリプトを彷彿とさせるような書き方をコンセプトとしています。
Eagleで鍵となるのはシーンに配置されるActorクラスとActorの振る舞いを決定するComponentクラスの二つです。
これらはアプリの見た目や処理を一手に担う重要なクラスなので次項で説明していきます。
以下のリンクから開発中のソースコードを見ることができます。
(READMEとかいろいろ不足していますがこれから足すので許してください)
Eagle for Siv3D GitHub
Actor
Unityで言うところのGameObjectに相当します。
基本的な特徴として以下のものがあります。
- 必ずシーンに所属する
- 固有の名前を持つ
- 複数のタグを持てる
- コンポーネントのアタッチ・取得・ディタッチ
- アタッチされた全コンポーネントの有効・無効
- Actor自身にふるまいは設定されていない
Actorの生成そのものは簡単です。
Eagle側で用意したBasicSceneクラスを継承してシーンを作成し、その中でcreateActor(String)関数を呼びだすだけです。
#include <Eagle.hpp>
// シーンの作成
class TestScene : public eagle::DefaultWorld::BasicScene
{
public:
TestScene(const InitData& _ini) : BasicScene(_ini)
{
// MyActorという名前のActorを生成
createActor(U"MyActor");
}
void update()override
{
}
};
void Main()
{
// シーンを管理するマネジメントクラスを生成
eagle::DefaultWorld world{};
// TestSceneをMySceneという名前でworldに登録
world.add<TestScene>(U"MyScene");
while(s3d::System::Update())
{
if(not world.update())
break;
}
}
しかし上記のソースコードだけでは真っ暗なウィンドウが映ってしまうでしょう。
そこでコンポーネントの出番がやってきます。
Component
ComponentはActorの動作を定義付けます。
Actorがどのような姿をしていて、どのように動くのか。Actorの振る舞いはコンポーネント一つで大きく変わります。
特徴としては以下のものがあります。
- 必ずActorに紐づけされている
- 三種類のupdate関数を持つ
- 継承を前提としたいくつかの仮想関数を持つ
しかしデフォルトのComponentクラスはActorにアタッチさせることができません。
これはComponentが抽象的なクラスであるためです。抽象クラスについての詳しい説明は今回は省きます。
しかしこのままではActorは何者でもないままです。
なので円を描画するコンポーネントとキーの入力に従って移動するコンポーネントを作ってみましょう。
円を描画するコンポーネント
#pragma once
#include <Eagle.hpp>
// 2D描画を可能にするためのコンポーネントを継承
class CircleComponent : public eagle::DrawableComponent2D
{
public:
// コンストラクタ
CircleComponent();
// Actorの座標に描画する円情報を設定
void setCircle(const Circle& _circle);
// 描画する円の色を設定
void setColor(const ColorF& _color);
// 描画関数
void draw()const override;
private:
// 円の情報
// posはActor座標からのオフセットとして扱う
Circle mCircle;
// 円の色
ColorF mColor;
};
#include "CircleComponent.hpp"
CircleComponent::CircleComponent()
: mCircle(0,0,10)
, mColor(ColorF{1.0})
{
}
void CircleComponent::setCircle(const Circle& _circle)
{
mCircle = _circle;
}
void CircleComponent::setColor(const ColorF& _color)
{
mColor = _color;
}
void CircleComponent::draw()const
{
// Actorの2Dワールド座標を取得する
Vec2 pos = getTransform()->getWorldPos2D();
// Actorの座標に円を描画する
mCircle.movedBy(pos).draw(mColor);
}
キー入力で移動するコンポーネント
#pragma once
#include <Eagle.hpp>
class MoveComponent : public eagle::Component
{
public:
// コンストラクタ
MoveComponent();
// Actorの移動速度を設定
void setSpeed(double _speed);
// 入力と移動の更新
void update()override;
private:
// 一秒に進む速度
double mSpeed;
};
#include "MoveComponent.hpp"
MoveComponent::MoveComponent()
: mSpeed(300.0)
{
}
void MoveComponent::setSpeed(double _speed)
{
mSpeed = _speed;
}
void MoveComponent::update()
{
Vec2 input{ 0,0 };
if(KeyUp.pressed())input.y -= 1.0;
if(KeyDown.pressed())input.y += 1.0;
if(KeyLeft.pressed())input.x -= 1.0;
if(KeyRight.pressed())input.x += 1.0;
// 入力が無ければこれ以上の処理はしない
if(input.isZero())
return;
// Actorが移動する方向
const Vec2 Direction = input.normalized();
// このフレームでの移動量
const double Velocity = mSpeed * Scene::DeltaTime();
// Directionの方向にVelocityの分だけ移動する
getTransform()->translate2D(Direction * Velocity);
}
Actorの生成と作ったComponentのアタッチ
#include "CircleComponent.hpp"
#include "MoveComponent.hpp"
// シーンの作成
class TestScene : public eagle::DefaultWorld::BasicScene
{
public:
TestScene(const InitData& _ini) : BasicScene(_ini)
{
// MyActorという名前のActorを生成
auto actor = createActor(U"MyActor").lock();
// Actorの座標をシーンの中心に設定する
actor->getTransform()->setPos(Scene::CenterF());
// CircleComponentをMyActorに取り付ける
auto circleComponent = actor->attachComponent<CircleComponent>().lock();
circleComponent->setCircle(Circle{0,0,20});
circleComponent->setColor(Palette::Orange);
// MoveComponentをMyActorに取り付ける
auto moveComponent = actor->attachComponent<MoveComponent>().lock();
moveComponent->setSpeed(300.0);
}
void update()override
{
// キー入力をわかりやすくする
{
if(KeyUp.pressed())Print << U"[↑]";
if(KeyDown.pressed())Print << U"[↓]";
if(KeyLeft.pressed())Print << U"[←]";
if(KeyRight.pressed())Print << U"[→]";
}
}
};
void Main()
{
// シーンを管理するマネジメントクラスを生成
eagle::DefaultWorld world{};
// TestSceneをMySceneという名前でworldに登録
world.add<TestScene>(U"MyScene");
while(s3d::System::Update())
{
ClearPrint();
if(not world.update())
break;
}
}
lock関数について
createActorやattachComponentの戻り値は弱参照となっており、そのままではそのクラスにアクセスできません。
そこでlock関数を用いることでアクセスできるようになります。
Eagleでは意図しない参照の増加を防ぐのと無効な参照を判定するために、より安全なものを提供しています。
lock関数の詳細についてはこちらのページをご覧ください。
上にあるサンプルコードをすべて実装するとこのようになります。
(カクカクしているのはGifに変換した際のもです)
この程度であればSiv3Dを使って直接描いたほうが速いのですが、開発規模が多きくなっていくとそうもいきません。
たとえばこの円から弾が飛び出す仕様が追加されたら? 移動するたびにキラキラエフェクトが出る使用が追加されたら?
この調子でMain.cpp一筋となると中々辛いものです。
なにより急な仕様変更に対する時間的なコストが大きすぎます。
もちろんコードの組み方次第でどうとでもなる問題ではありますが少しでも楽に越したことはありませんからね。
さいごに
いかがでしたでしょうか。
初めのころはDirectX12で作ってやろうと思っていましたが、その難易度の高さに絶望していました。
そんな折にSiv3Dを見つけて食らいついて今では以前よりアクティブな自分がいることに驚きです。
これからもSiv3Dが発展していくのに合わせて開発していけたらと思っています。
目指すはコードベースの大規模開発用ゲームエンジンです。
それでは、
ここまでご覧いただきありがとうございました。