結論
ゲーム画面のシーンに複数のカメラとキャンバスを作成して当たり判定が視覚通りに反応するよう設計しました.
↓ 執筆時点のリポジトリ
(Doxygenフォーマットでコメントを入れています)
はじめに
本記事では,DXライブラリを用いた多カメラ・多スクリーンシステムの設計と実装について解説します.前回は,ゲームのアプリケーションクラスにてシングルトンパターンを利用しました.(理由等はコメントの議論も参照)
大まかな実装一覧として
- カメラ座標への入力変換
- レンダリングのターゲット管理
- ドラッグ操作(コンポーネント管理)
があります.
環境
- Windows 11(24H2)
- Visual Studio 2022
- C++20
- DXライブラリ ver. 3.24d
背景
1つの表示領域に複数の異なる情報・操作を提示する必要があったためです.メインウインドウがあってサブウインドウとして自由に配置できるミニマップがあったり,インターフェースのプレビューを表示したりなど...使い方は様々です.
システム概要
主な構成
本システムの構成は次の通りです.
- Application: エントリーポイント,ライブラリの初期化など
- SceneManager / Scene: 遷移や別画面(設定含む)処理含むシーン管理
- GameObject:
Scene
で保持されるオブジェクトの単位 - 各Components:
GameObject
に対して付与可能なクラス,ループ処理のメインとなる部分 - Camera2DComponent / MouseCameraSelector: カメラの管理と1スクリーン多カメラの場合のオブジェクト選択
コンポーネントシステムをメインに書いており,必要に応じて自由に追加できる構成にしています.
記事内の単語
あらかじめ
- 多カメラ: シーン(DX_SCREEN_BACK)とは別にグラフィックハンドルが設けられ,それを特定の範囲で切り出してある場所へ配置するいくつかのオブジェクト
- 多スクリーン: シーンとは別のいくつかあるグラフィックハンドル
- 入力変換: グラフィック内のマウス座標をカメラで投影される元のスクリーン(グラフィックハンドル)上の座標へ変換すること
実際の設計
ゲームオブジェクト
まずは,根本的なシステムを実現するためにゲームオブジェクト(ベース部分)から設計します.
このクラスはシーンで配列(std::list
)として保持され,任意のタイミングで追加/削除ができるような構成にします.
std::listを使った経緯
最初はstd::vector
を使ってましたが,push_back
やemplace_back
はcapacity
を超えると再確保し,ポインタも割り当てなおされます.これを,GameObjectのアップデート中に呼び出されるとポインタが壊れて例外をスローする可能性があります.std::list
は値と次へのポインタを持つ構造なので途中で再割り当てされることがないことから使用しています.
もし,std::vector
を使い続けたい場合は,後述のScene::ProcessPending
のようにループ外で処理できるよう待機リストを作成すれば良いかもしれません.
using GameObjectPtr = std::shared_ptr<GameObject>;
using ComponentPtr = std::shared_ptr<Component>;
class GameObject : public std::enable_shared_from_this<GameObject> {
std::unordered_map<std::type_index, ComponentPtr> components_;
bool shouldDestroy = false;
int layer = 0;
int orderInLayer = 0;
bool active = true;
bool visible = true;
public:
std::string name;
// 初期化
GameObject(std::string _name = "GameObject");
// コンポーネントの追加
template <typename T, typename... Args>
requires std::is_constructible_v<T, Args...>
std::shared_ptr<T> AddComponent(Args&&... args) {
auto comp = std::make_shared<T>(std::forward<Args>(args)...);
comp->SetGameObject(shared_from_this());
components_[typeid(T)] = comp;
return comp;
}
// コンポーネントの取得
template <typename T>
std::shared_ptr<T> GetComponent() {
auto it = components_.find(typeid(T));
return (it != components_.end()) ? std::dynamic_pointer_cast<T>(it->second)
: nullptr;
}
void SetOrderInLayer(int order) { orderInLayer = order; }
int GetOrderInLayer() const { return orderInLayer; }
void SetLayer(int _layer) { layer = _layer; }
int GetLayer() const { return layer; }
void Destroy(); // 子コンポーネントも含めて破棄
bool ShouldBeDestroyed() const { return shouldDestroy; }
// active / visible のゲッターセッター
void Update() { // 有効でないならパス
if (!active) return;
for (auto& [type, component] : components_) {
component->Update();
}
}
void Render() { // 見えなかったり有効でなかったらパス
if (!active || !visible) return;
for (auto& [type, component] : components_) {
component->Render();
}
}
};
ゲームオブジェクトではコンポーネントのコンテナを持つことでオブジェクトに拡張性を持たせています.Component
クラスを継承すればよいので,UnityのC#スクリプトやRigidbodyをオブジェクトに追加する感覚ととらえてください.
std::unordered_map<std::type_index, ComponentPtr> components_;
// コンポーネントの追加
template <typename T, typename... Args>
requires std::is_constructible_v<T, Args...>
std::shared_ptr<T> AddComponent(Args&&... args) {
auto comp = std::make_shared<T>(std::forward<Args>(args)...);
comp->SetGameObject(shared_from_this());
components_[typeid(T)] = comp;
return comp;
}
// コンポーネントの取得
template <typename T>
std::shared_ptr<T> GetComponent() {
auto it = components_.find(typeid(T));
return (it != components_.end()) ? std::dynamic_pointer_cast<T>(it->second)
: nullptr;
}
std::unordered_map
として,キーにtype_index
を用いてポインタを格納しています.AddComponent
はテンプレートにより型を指定して引数へコンストラクタ引数をそのまま渡す形(完全転送)にしています.反対にGetComponent
はテンプレートで型を指定すると派生コンポーネントのポインタが返ってくるようにしています.また,AddComponent
は返り値に作成した派生コンポーネントを返します(後で示すpublicメンバ/メソッドを操作するため).
GameObjectPtr rect = std::make_shared<GameObject>("rect");
rect->AddComponent<TransformComponent>(0.f /*x*/, 0.f /*y*/); // TransformComponentの場合
コンポーネントのベース
次にゲームオブジェクトへ追加していくコンポーネントの抽象クラスを作成します.
class Component {
protected:
// コンポーネントを保持するゲームオブジェクト(弱参照) 循環参照対策
std::weak_ptr<GameObject> gameObject;
public:
virtual ~Component() = default;
virtual void Update() {};
virtual void Render() {};
// コンポーネントとゲームオブジェクトを紐づける
void SetGameObject(std::shared_ptr<GameObject> obj) { gameObject = obj; }
std::shared_ptr<GameObject> GetGameObject() const {
return gameObject.lock();
}
};
Update
とRender
の関数は=0
にしていないため純粋仮想関数ではありません.オーバーライドしなければ何も処理のない関数として流れていきます.
↑ virtual
とシンプルな仮想関数なので再定義(オーバーライド)されて初めて仕事をします.(つまり厳密には抽象クラスではない)
以降にコンポーネントが付くクラスは全てこのクラスの継承となります.
座標コンポーネント
システムオブジェクトでない限り座標の表現で必須なコンポーネントです.Unityお馴染みの子オブジェクト要素はここで定義します.
class TransformComponent : public Component {
public:
// 親オブジェクト(弱参照) 循環参照対策
std::weak_ptr<GameObject> parent;
// 子オブジェクト配列
std::list<std::shared_ptr<GameObject>> children;
// ローカル座標
float localX, localY;
float localRotation;
float localScaleX, localScaleY;
// ワールド座標
float worldX, worldY;
float worldRotation;
float worldScaleX, worldScaleY;
TransformComponent(float x = 0.f, float y = 0.f, float rotation = 0.f,
float scaleX = 1.f, float scaleY = 1.f);
// 親オブジェクトの登録
void SetParent(std::shared_ptr<GameObject> newParent);
// ワールド座標の再計算 (Updateで通常は呼び出し)
void UpdateWorldTransform();
void Update() override;
};
このクラスのようにコンポーネントのメンバは基本的にpulic
としました.処理以外は公開することで柔軟性が向上するようにしています.ドラッグなどで移動を反映させる変数はローカル座標であってグローバル座標は触らないようにします.グローバル座標はparent
のグローバル座標と自身のローカル座標を組み合わせて計算します.
(.cpp) UpdateWorldTransformとUpdate
UpdateWorldTransform
についてworldX
やworldY
の計算はGIFの通り問題ないです.
void TransformComponent::UpdateWorldTransform() {
if (auto parentPtr = parent.lock()) {
if (auto parentTransform = parentPtr->GetComponent<TransformComponent>()) {
float pX = parentTransform->worldX;
float pY = parentTransform->worldY;
float pRot = parentTransform->worldRotation;
float pScaleX = parentTransform->worldScaleX;
float pScaleY = parentTransform->worldScaleY;
float rad = pRot * (static_cast<float>(M_PI) / 180.0f);
float rotatedX =
localX * pScaleX * cos(rad) - localY * pScaleY * sin(rad);
float rotatedY =
localX * pScaleX * sin(rad) + localY * pScaleY * cos(rad);
worldX = pX + rotatedX;
worldY = pY + rotatedY;
worldRotation = pRot + localRotation;
worldScaleX = pScaleX * localScaleX;
worldScaleY = pScaleY * localScaleY;
} else {
worldX = localX;
worldY = localY;
worldRotation = localRotation;
worldScaleX = localScaleX;
worldScaleY = localScaleY;
}
} else {
worldX = localX;
worldY = localY;
worldRotation = localRotation;
worldScaleX = localScaleX;
worldScaleY = localScaleY;
}
}
void TransformComponent::Update() { UpdateWorldTransform(); }
ワールド座標はそのループまたは次のループで更新されますが,トリガー等の処理でならlocalの更新後に直接呼ぶこともありかもしれません.
長方形描画コンポーネント
ここは手短に.TransformComponent
と引数でwidth
,height
とcolor
を受け取って長方形を描画します.
class Rect2DComponent : public Component {
public:
float sx, sy;
unsigned int color;
Rect2DComponent(float _sx, float _sy, unsigned int _color);
void Render() override;
};
(.cpp) Render
TransformComponent
を取得し,見つかった場合は描画します.
void Rect2DComponent::Render() {
auto transform = GetGameObject()->GetComponent<TransformComponent>();
if (!transform) return;
float x = transform->worldX;
float y = transform->worldY;
float width = sx * transform->worldScaleX;
float height = sy * transform->worldScaleY;
DrawBoxAA(x, y, x + width, y + height, color, true);
}
コンポーネントが取得できなかった場合にOutputDebugString
関数や(_DEBUG
において)assert
関数で開発時のヌルチェックは挟んだ方が良いかもしれません
当たり判定コンポーネント
あくまでバウンディングボックスを用意するだけのクラスです.このクラスを呼ぶことでマウスとの判定や(今回は実装していない)他オブジェクトとの衝突判定に活用できます.
class ColliderComponent : public Component {
public:
float width, height; // 途中で変えてもOK
ColliderComponent(float w, float h);
// 判定関数
bool Contains(float x, float y);
};
Update
やRender
はオーバーライドしていないので,フレーム単位の更新処理はありません.Contains
は取得されたコンポーネントで呼び出されbool
を返します.引数でキャンバスの座標を受け取りバウンディングボックス内かを判定します.
(.cpp) Contains
いつもの長方形判定
bool ColliderComponent::Contains(float x, float y) {
auto transform = GetGameObject()->GetComponent<TransformComponent>();
if (!transform) return false;
float worldX = transform->worldX;
float worldY = transform->worldY;
return (x >= worldX && x <= worldX + width && y >= worldY &&
y <= worldY + height);
}
カメラ(2D)コンポーネント
キャンバスに映っているオブジェクトをメインスクリーン(DX_SCREEN_BACK)へ映すコンポーネントです.
class Camera2DComponent : public Component {
public:
int srcX, srcY, srcWidth, srcHeight; // キャンバスの切り抜く範囲の指定
int destX = 0, destY = 0, destWidth, destHeight; // カメラでの描画範囲指定
int renderLayer; // カメラで投影する対象のレイヤー
Camera2DComponent(int _srcX, int _srcY, int _srcWidth, int _srcHeight,
int _destWidth, int _destHeight, int _renderLayer = 0);
void Update() override;
// スクリーンハンドルで描画する
void Render(int offscreenHandle) const;
};
Renderとオーバーライドについて
執筆時点での話ですが,Render
関数はオーバーライドしていません.
void Camera2DComponent::Render(int offscreenHandle) const {
DrawRectExtendGraph(destX, destY, destX + destWidth, destY + destHeight, srcX,
srcY, srcWidth, srcHeight, offscreenHandle, TRUE);
}
DXライブラリのMakeScreen等で作成されたグラフィックハンドルは描画関数でハンドルの引数として渡せますが,レイヤーとは異なるためrenderLayer
は渡せません.したがって,後述のScene::Renderで渡している現状です.
(レイヤーそのものを入れ替える仕様がまだないので,その仕様が追加された際に破壊的変更アップデートしようと考えています.)
シーン
シーンの定義と管理にも触れたいと思います.
抽象クラスのScene
は次の通りです.
class Scene : public std::enable_shared_from_this<Scene> {
protected:
std::list<GameObjectPtr> gameObjects; // シーン内の登録済みオブジェクト
std::shared_ptr<MouseCameraSelector> cameraSelector;
std::unordered_map<int, std::shared_ptr<RenderTarget>> renderTargets; // グラフィックハンドルリスト
bool isAdditive = false; // 追加用のシーンかどうか
public:
Scene() = default;
virtual ~Scene() = default;
virtual void Update();
virtual void Render();
virtual void Start() = 0;
virtual void Reset();
void AddObject(GameObjectPtr obj); // 呼ばれないとレンダリングされない
void RegisterRenderTarget(int layer, int width, int height); // キャンバスの作成
void SetAdditive(bool additive) { isAdditive = additive; }
bool IsAdditive() const { return isAdditive; } // DX_SCREEN_BACK初期化管理
void SetCameraSelector(
std::shared_ptr<MouseCameraSelector> _cameraSelector);
// 重複判定に使える
const std::list<GameObjectPtr>& GetGameObjects() const;
private:
void ProcessPending();
};
リポジトリ(または上のGIF)ではTopScene
が継承してcurrentScene
として扱われていますがTopSceneでは新たにコールバック関数を用意するくらいでメソッドやメンバの追加は基本ありません.
(.cpp) Update と Render
処理について,Update
は全部まとめて回していますが,Render
はキャンバスに描画してからカメラで投影する以上,先にカメラ以外を描画する仕組みが必要でした.したがって,フィルター後にレイヤーとレイヤー内順序でソートして描画→カメラのレイヤー順で描画という2段構えになっています.
void Scene::Update() {
for (auto& obj : gameObjects) {
obj->Update();
}
ProcessPending();
}
void Scene::Render() {
for (auto& [layer, renderTarget] : renderTargets) {
SetDrawScreen(renderTarget->handle);
ClearDrawScreen();
std::vector<GameObjectPtr> layerObjects;
for (auto& obj : gameObjects) {
// Camera2DComponentを持たないものを対象
if (obj->GetLayer() == layer && !obj->GetComponent<Camera2DComponent>()) {
layerObjects.push_back(obj);
}
}
std::sort(layerObjects.begin(), layerObjects.end(),
[](const GameObjectPtr& a, const GameObjectPtr& b) {
return a->GetOrderInLayer() < b->GetOrderInLayer();
});
for (auto& obj : layerObjects) {
obj->Render();
}
}
// Camera2DComponentを持つオブジェクト
struct RenderCommand {
int order;
int objectLayer;
std::function<void()> command;
};
std::vector<RenderCommand> renderQueue;
std::vector<GameObjectPtr> cameraObjects;
for (auto& obj : gameObjects) {
if (obj->GetComponent<Camera2DComponent>()) {
cameraObjects.push_back(obj);
}
}
for (auto& camObj : cameraObjects) {
auto camComp = camObj->GetComponent<Camera2DComponent>();
if (camComp) {
int layer = camComp->renderLayer;
if (renderTargets.find(layer) != renderTargets.end()) {
int order = camObj->GetOrderInLayer();
int objectLayer = camObj->GetLayer();
int rtHandle = renderTargets[layer]->handle;
renderQueue.push_back({order, objectLayer, [camComp, rtHandle]() {
camComp->Render(rtHandle);
}});
}
}
}
// 描画順にソート
std::sort(renderQueue.begin(), renderQueue.end(),
[](const RenderCommand& a, const RenderCommand& b) {
if (a.objectLayer == b.objectLayer) return a.order < b.order;
return a.objectLayer < b.objectLayer;
});
SetDrawScreen(DX_SCREEN_BACK);
if (!isAdditive) {
ClearDrawScreen();
}
for (auto& cmd : renderQueue) {
cmd.command();
}
}
また,遷移管理はSceneのポインタ(currentScene)で行っています.ChangeScene
とAdditiveScene
のように2つへ用途を分けisAdditive
の付与を外で行っています.現在はインスタンスを受け取って管理していますが,AddComponent
のようにテンプレート型で受け取って格納する方法がUnload
のメソッドを活かす際に有効ではと考えています.
重複判定・ドラッグコンポーネント
多カメラのシステムでカメラが重なっている場合,どのカメラに注目すればよいか不明になります.今回はMouseCameraSelector
を定義し,マウスポインタがカメラ内にあるか判定し,どのカメラと入力変換を対応付けるか決定します.
class MouseCameraSelector {
public:
std::vector<std::shared_ptr<GameObject>> cameras;
std::shared_ptr<CameraMouseCoordinateConverter> GetCurrentMouseConverter();
std::shared_ptr<CameraMouseCoordinateConverter> GetCurrentMouseConverter(
int targetLayer);
};
マウス座標を取得後,カメラを1個ずつループで回し「範囲内にあり,かつ最もレイヤーまたは順序が高いもの」を引数としてコンバーターのインスタンスを返します.
(.cpp) GetCurrentMouseConverter
std::shared_ptr<CameraMouseCoordinateConverter>
MouseCameraSelector::GetCurrentMouseConverter(int targetLayer) {
int mouseX, mouseY;
if (auto mouseProvider = InputManager::GetInstance().GetMouseProvider()) {
mouseProvider->GetMousePosition(mouseX, mouseY);
} else {
GetMousePoint(&mouseX, &mouseY);
}
std::shared_ptr<Camera2DComponent> selectedCamera = nullptr;
for (auto& camObj : cameras) {
auto camComp = camObj->GetComponent<Camera2DComponent>();
if (!camComp) continue;
if (camComp->renderLayer != targetLayer) continue;
int dx = camComp->destX;
int dy = camComp->destY;
int dWidth = camComp->destWidth;
int dHeight = camComp->destHeight;
if (mouseX >= dx && mouseX <= dx + dWidth && mouseY >= dy &&
mouseY <= dy + dHeight) {
if (!selectedCamera ||
(camComp->GetGameObject()->GetLayer() >
selectedCamera->GetGameObject()->GetLayer()) ||
(camComp->GetGameObject()->GetLayer() ==
selectedCamera->GetGameObject()->GetLayer() &&
(camComp->GetGameObject()->GetOrderInLayer() >
selectedCamera->GetGameObject()->GetOrderInLayer()))) {
selectedCamera = camComp;
}
}
}
if (selectedCamera) {
return std::make_shared<CameraMouseCoordinateConverter>(selectedCamera);
} else {
return nullptr;
}
}
CameraMouseCoordinateConverter
はマウス座標をカメラ内キャンバス座標へ変換するクラスです.Convert
メソッドを実行してキャンバス内の座標を取得後,ついにボタンの接触判定が可能になります.
すなわち,MouseCameraSelector
へカメラを登録しなければ当たり判定を持たないカメラ(ex:ミニマップカメラ)ができあがります.
class CameraMouseCoordinateConverter {
public:
std::shared_ptr<Camera2DComponent> pCamera_;
CameraMouseCoordinateConverter(std::shared_ptr<Camera2DComponent> _pCamera)
: pCamera_(_pCamera) {}
void Convert(int screenX, int screenY, int& outX, int& outY) const {
int relativeX = screenX - pCamera_->destX;
int relativeY = screenY - pCamera_->destY;
float scaleX = static_cast<float>(pCamera_->srcWidth) / pCamera_->destWidth;
float scaleY =
static_cast<float>(pCamera_->srcHeight) / pCamera_->destHeight;
outX = pCamera_->srcX + static_cast<int>(relativeX * scaleX);
outY = pCamera_->srcY + static_cast<int>(relativeY * scaleY);
}
};
今回はカメラが回転していることを想定しません.
キャンバス座標を得られたのでドラッグコンポーネントを用意します.
class DragComponent : public Component {
public:
bool wasLeftMouseDown = false; // 前フレームで押されていたか
bool dragging = false; // ドラッグ中か
int offsetX = 0; // ドラッグ開始時にオフセットを設定する
int offsetY = 0; // ドラッグ開始時にオフセットを設定する
// オプション
bool ignoreLayerCheck = false;
bool cancelDraggingOnConverterNull = false;
std::shared_ptr<class TransformComponent> targetTransform;
// 代入必須
std::shared_ptr<MouseCameraSelector> cameraSelector;
DragComponent() = default;
void Update() override;
};
オプションについて
-
ignoreLayerCheck
: レイヤー構造,上に物が重なっていても関係なく掴める -
cancelDraggingOnConverterNull
: カメラの外,コンバーターがnullptr
の場合に掴みをキャンセルにするか(ない場合はカメラを飛び越えて運んだり,クリック状態の保持ができたりする) -
targetTransform
: 本来は自身のローカル座標を変更するものだが,設定すると他オブジェクトのローカル座標でドラッグさせることができるようになる(ex: スクリーンキーボードならぬスクリーントラックパッド?)
(.cpp) Update
仕組みは単純で,
- マウス座標を取得
- 入力変換用コンバータ取得
- マウス座標を変換
- (レイヤー構造を見る設定の場合,後述の最前オブジェクト判定で自身が指されているか確認する)
- クリック中はローカル座標を変更する
void DragComponent::Update() {
auto defaultTransform = GetGameObject()->GetComponent<TransformComponent>();
auto collider = GetGameObject()->GetComponent<ColliderComponent>();
if (!defaultTransform || !collider) return;
auto activeTransform = targetTransform ? targetTransform : defaultTransform;
int x = static_cast<int>(activeTransform->worldX);
int y = static_cast<int>(activeTransform->worldY);
int mouseScreenX, mouseScreenY;
if (auto mouseProvider = InputManager::GetInstance().GetMouseProvider()) {
mouseProvider->GetMousePosition(mouseScreenX, mouseScreenY);
} else {
GetMousePoint(&mouseScreenX, &mouseScreenY);
}
std::shared_ptr<CameraMouseCoordinateConverter> converter;
if (cameraSelector) {
converter =
cameraSelector->GetCurrentMouseConverter(GetGameObject()->GetLayer());
}
// wasLeftMouseDown は converterによるreturnの前で管理する方が良いかもしれない
if (!converter) {
if (dragging && cancelDraggingOnConverterNull) {
dragging = false;
}
return;
}
int convertedX, convertedY;
converter->Convert(mouseScreenX, mouseScreenY, convertedX, convertedY);
bool isOver = collider->Contains(static_cast<float>(convertedX),
static_cast<float>(convertedY));
bool currentLeftDown = (GetMouseInput() & MOUSE_INPUT_LEFT) != 0;
if (!dragging) {
// クリック開始時にドラッグ開始
if (!wasLeftMouseDown && currentLeftDown && isOver) {
if (!ignoreLayerCheck) {
auto topObj = Application::GetInstance().GetTopGameObjectAtPoint();
if (!topObj || topObj.get() != GetGameObject().get()) {
wasLeftMouseDown = currentLeftDown;
return;
}
}
dragging = true;
offsetX = convertedX - x;
offsetY = convertedY - y;
}
} else {
if (currentLeftDown) {
activeTransform->localX = static_cast<float>(convertedX - offsetX);
activeTransform->localY = static_cast<float>(convertedY - offsetY);
} else {
// マウスボタン離した時
dragging = false;
}
}
wasLeftMouseDown = currentLeftDown;
}
レイヤー構造を見る場合,そのマウス座標で一番上にあるコライダーオブジェクトを取得して一致するか確認する必要が出てきました.MousePicker
のGetTopGameObjectAtPoint
メソッドで判定します.
class MousePicker {
public:
std::shared_ptr<GameObject> GetTopGameObjectAtPoint(
const std::vector<std::shared_ptr<Scene>>& scenes, int mouseScreenX,
int mouseScreenY,
const std::shared_ptr<MouseCameraSelector>& cameraSelector);
};
(.cpp) GetTopGameObjectAtPoint ('25/2/23更新)
次のような手順で最前オブジェクトを取得します.
- マウス座標を取得する
- AdditiveScene(上から覆いかぶせるシーン)も含めすべてのコライダーコンポーネントを構造体[
オブジェクト
,カメラレイヤー
,カメラオーダー
,オブジェクトオーダー
,コンバータ
]として配列を形成する - レイヤー順(一致するならオーダー順)で並び替える
- レイヤーが高い順からループを回し,コンバートされたキャンバス座標でコライダーの
Contains
が真値になり次第,そのオブジェクトを返して終了
std::shared_ptr<GameObject> MousePicker::GetTopGameObjectAtPoint(
const std::vector<std::shared_ptr<Scene>>& scenes, int mouseScreenX,
int mouseScreenY,
const std::shared_ptr<MouseCameraSelector>& cameraSelector) {
struct Clickable {
std::shared_ptr<GameObject> obj;
int cameraLayer = 0;
int cameraOrder = 0;
int objOrder = 0;
std::shared_ptr<CameraMouseCoordinateConverter> converter;
};
std::vector<Clickable> clickables;
for (const auto& scene : scenes) {
for (const auto& obj : scene->GetGameObjects()) {
if (!obj->IsActive()) continue;
auto collider = obj->GetComponent<ColliderComponent>();
if (!collider) continue;
if (obj->GetComponent<Camera2DComponent>()) continue;
Clickable c;
c.converter = cameraSelector->GetCurrentMouseConverter(obj->GetLayer());
if (!c.converter) {
continue;
}
auto cameraObj = c.converter->pCamera_->GetGameObject();
c.obj = obj;
c.objOrder = obj->GetOrderInLayer();
c.cameraLayer = cameraObj->GetLayer();
c.cameraOrder = cameraObj->GetOrderInLayer();
clickables.push_back(c);
}
}
std::sort(clickables.begin(), clickables.end(),
[](const Clickable& a, const Clickable& b) {
return std::tie(a.cameraLayer, a.cameraOrder, a.objOrder) >
std::tie(b.cameraLayer, b.cameraOrder, b.objOrder);
});
int convertedX, convertedY;
for (auto& c : clickables) {
if (!c.converter) continue;
c.converter->Convert(mouseScreenX, mouseScreenY, convertedX, convertedY);
if (c.obj->GetComponent<ColliderComponent>()->Contains(
static_cast<float>(convertedX), static_cast<float>(convertedY))) {
return c.obj;
}
}
return nullptr;
}
'25/2/23 更新
オブジェクトレイヤーは使わず,カメラレイヤーで判定しています.カメラはキャンバス上のレイヤーを超えて好きな場所へ配置が可能です.すると,表示上本来のレイヤーと異なるわけでカメラへ完全依存となります.
従って,比較の優先順位は以下のようになります.
カメラのレイヤー → カメラのオーダー → オブジェクトのオーダー
次章でサンプル画面を構築していますが,
画像内の
一番上にいるキャンバス(「カメラ3描画部分」と全部見えるもの)は
キャンバス3,カメラレイヤー5,カメラオーダー-10
です.
ウインドウの縁のように見せている黒い帯があるキャンバスは
キャンバス4,カメラレイヤー4,カメラオーダー0
です.
ドラッグ可能な赤いオブジェクト(レイヤー(描画キャンバス)3,オーダー5
)の下に,
ドラッグ可能な黒いオブジェクト(レイヤー(描画キャンバス)4,オーダー-100
)がある場合,
オブジェクトのレイヤーとオーダーの比較では黒いオブジェクトを掴めばよいとなってしまい,カメラで上に来ていることが考慮されていません.
そのため,カメラレイヤーとそのオーダーを先に比較する必要があります.
これで,レイヤー無視/無視しないオプションが付け加え可能なシステムとして最上位オブジェクト取得が完成しました.ただし一度呼び出すたび,3重ループ(シーン,オブジェクト,カメラ)になり負荷がかかります.クリックした最初の判定でチェックするといった負荷軽減を組み込むことも重要になりそうです.(現在はボタンとドラッグで適用しています)
additiveScene
はベースのシーンより上だから...な処理は用意しませんでした.「敢えて」上から出てきたシーンの一部が下側へ行ってもいいような設計です.
シーンの重なりを考慮する場合は,Clickable
構造体にSceneの順番を記録しソートへ組み込むだけで対応可能です.
OrderInLayer
まで完全一致すると視覚通りに判定されない場合があります.
- オブジェクトの追加する順番が決して描画順でない
-
std::sort
が不安定なソート(クイックソート・イントロソート)である
サンプルの画面設計
この画面の設計を行った後,課題を述べます.リポジトリではTopSceneに該当します.
class TopScene : public Scene {
public:
TopScene() {}
void Start() override;
private:
void OnButtonClickedMember();
};
OnButtonClickedMember
はコールバック用メソッドです.GIF内のランダムなレイヤーや位置に正方形が出る処理にあたります.ラムダ関数で実装しても問題なく動作します.先述の通り,TopSceneで特別用意したメンバ等はありません.
(.cpp) Start,OnButtonClickedMember
とにかく長いので折りたたんでいます.
Unityのヒエラルキーやインスペクターがここに該当します.したがって,AddObject
やAddComponent
を多用しています.
void TopScene::Start() {
auto CreateObject = [](const std::string& name) -> GameObjectPtr {
return std::make_shared<GameObject>(name);
};
// 予め使う分を宣言
int meiryo20 = ResourceManager::GetInstance().LoadFont("Meiryo", 20, -1);
int meiryo30 = ResourceManager::GetInstance().LoadFont("Meiryo", 30, -1);
RegisterRenderTarget(0, 1280, 720);
// カメラ1 と カメラ2の定義: 浮遊用
RegisterRenderTarget(1, 1000, 1000);
RegisterRenderTarget(2, 1280, 720);
RegisterRenderTarget(3, 500, 500);
RegisterRenderTarget(4, 1280, 720);
{
// カメラ: 背景描画用
GameObjectPtr camera0 = CreateObject("camera0");
camera0->SetLayer(0);
camera0->AddComponent<TransformComponent>(0.f, 0.f);
camera0->AddComponent<Camera2DComponent>(0, 0, 1280, 720, 1280, 720,
camera0->GetLayer());
AddObject(camera0);
cameraSelector->cameras.push_back(camera0);
// 背景: カメラ0
GameObjectPtr camera0BackRect = CreateObject("camera0BackRect");
camera0BackRect->SetLayer(camera0->GetLayer());
camera0BackRect->SetOrderInLayer(-100);
camera0BackRect->AddComponent<TransformComponent>();
camera0BackRect->AddComponent<Rect2DComponent>(1280.f, 720.f,
GetColor(239, 241, 243));
camera0BackRect->AddComponent<TextComponent>(
"カメラ0描画部分", meiryo30, GetColor(100, 100, 100));
AddObject(camera0BackRect);
GameObjectPtr rectSpawningButton0 = CreateObject("rectSpawningButton0");
rectSpawningButton0->SetLayer(camera0->GetLayer());
rectSpawningButton0->SetOrderInLayer(5);
rectSpawningButton0->AddComponent<TransformComponent>(100.f, 400.f);
auto rectSpawningButton0DragComp =
rectSpawningButton0->AddComponent<DragComponent>();
rectSpawningButton0->AddComponent<ColliderComponent>(250.f, 150.f);
rectSpawningButton0->AddComponent<Rect2DComponent>(250.f, 150.f,
GetColor(118, 137, 72));
rectSpawningButton0->AddComponent<TextComponent>(
"[ドラッグ可能]\nレイヤー0オブジェクト", meiryo20,
GetColor(10, 10, 10), 250, 45);
rectSpawningButton0DragComp->cameraSelector = cameraSelector;
rectSpawningButton0DragComp->cancelDraggingOnConverterNull = true;
AddObject(rectSpawningButton0);
}
{
// カメラ1: 浮遊ウインドウのメイン部分
GameObjectPtr camera1 = CreateObject("camera1");
camera1->SetLayer(1);
auto camera1TransComp =
camera1->AddComponent<TransformComponent>(0.f, 40.f);
camera1->AddComponent<Camera2DComponent>(0, 0, 1000, 1000, 500, 500,
camera1->GetLayer());
AddObject(camera1);
cameraSelector->cameras.push_back(camera1);
// 背景: カメラ1
GameObjectPtr camera1BackRect = CreateObject("camera1BackRect");
camera1BackRect->SetLayer(camera1->GetLayer());
camera1BackRect->SetOrderInLayer(-100);
camera1BackRect->AddComponent<TransformComponent>();
camera1BackRect->AddComponent<ColliderComponent>(1000.f, 1000.f);
camera1BackRect->AddComponent<Rect2DComponent>(1000.f, 1000.f,
GetColor(219, 211, 216));
camera1BackRect->AddComponent<TextComponent>("カメラ1描画部分", meiryo30,
GetColor(40, 40, 40));
AddObject(camera1BackRect);
GameObjectPtr rectSpawningButton1 = CreateObject("rectSpawningButton1");
rectSpawningButton1->SetLayer(camera1->GetLayer());
rectSpawningButton1->SetOrderInLayer(5);
rectSpawningButton1->AddComponent<TransformComponent>(100.f, 100.f);
auto rectSpawningButton1DragComp =
rectSpawningButton1->AddComponent<DragComponent>();
rectSpawningButton1->AddComponent<ColliderComponent>(250.f, 150.f);
rectSpawningButton1->AddComponent<Rect2DComponent>(250.f, 150.f,
GetColor(75, 136, 162));
rectSpawningButton1->AddComponent<TextComponent>(
"[ドラッグ可能]\nレイヤー1オブジェクト", meiryo30,
GetColor(10, 10, 10), 250, 45);
rectSpawningButton1DragComp->cameraSelector = cameraSelector;
rectSpawningButton1DragComp->cancelDraggingOnConverterNull = true;
AddObject(rectSpawningButton1);
// カメラ2: バナー部分
GameObjectPtr camera2 = CreateObject("camera2");
camera2->SetLayer(2);
camera2->AddComponent<TransformComponent>();
camera2->AddComponent<Camera2DComponent>(0, 0, 1280, 720, 1280, 720,
camera2->GetLayer());
AddObject(camera2);
cameraSelector->cameras.push_back(camera2);
// 背景: カメラ2
GameObjectPtr camera2BackRect = CreateObject("camera2BackRect");
camera2BackRect->SetLayer(camera2->GetLayer());
camera2BackRect->SetOrderInLayer(-100);
camera2BackRect->AddComponent<TransformComponent>(500.f, 60.f);
camera2BackRect->AddComponent<ColliderComponent>(500.f, 40.f);
auto camera2BackRectDragComp =
camera2BackRect->AddComponent<DragComponent>();
camera2BackRect->AddComponent<Rect2DComponent>(500.f, 40.f,
GetColor(34, 56, 67));
camera2BackRect->AddComponent<TextComponent>(
"[ドラッグ可能] カメラ2", meiryo30, GetColor(200, 200, 200));
camera2BackRectDragComp->cameraSelector = cameraSelector;
AddObject(camera2BackRect);
{ camera1TransComp->SetParent(camera2BackRect); }
}
{
// カメラ3: 浮遊ウインドウのメイン部分
GameObjectPtr camera3 = CreateObject("camera3");
camera3->SetLayer(3);
auto camera3TransComp =
camera3->AddComponent<TransformComponent>(0.f, 40.f);
camera3->AddComponent<Camera2DComponent>(0, 0, 500, 500, 500, 500,
camera3->GetLayer());
AddObject(camera3);
cameraSelector->cameras.push_back(camera3);
// 背景: カメラ3
GameObjectPtr camera3BackRect = CreateObject("camera3BackRect");
camera3BackRect->SetLayer(camera3->GetLayer());
camera3BackRect->SetOrderInLayer(-100);
camera3BackRect->AddComponent<TransformComponent>();
camera3BackRect->AddComponent<ColliderComponent>(500.f, 500.f);
camera3BackRect->AddComponent<Rect2DComponent>(500.f, 500.f,
GetColor(216, 203, 199));
camera3BackRect->AddComponent<TextComponent>(
"カメラ3描画部分", meiryo30, GetColor(40, 40, 40));
AddObject(camera3BackRect);
GameObjectPtr rectSpawningButton3 = CreateObject("rectSpawningButton3");
rectSpawningButton3->SetLayer(camera3->GetLayer());
rectSpawningButton3->SetOrderInLayer(5);
rectSpawningButton3->AddComponent<TransformComponent>(100.f, 100.f);
auto rectSpawningButton3DragComp =
rectSpawningButton3->AddComponent<DragComponent>();
rectSpawningButton3->AddComponent<ColliderComponent>(250.f, 200.f);
rectSpawningButton3->AddComponent<Rect2DComponent>(250.f, 200.f,
GetColor(187, 10, 33));
rectSpawningButton3->AddComponent<TextComponent>(
"[ドラッグ可能]\n[離トリガー]\nレイヤー3オブジェクト", meiryo30,
GetColor(10, 10, 10), 250, 45);
auto rectSpawningButton3ButtonComp =
rectSpawningButton3->AddComponent<ButtonComponent>();
rectSpawningButton3DragComp->cameraSelector = cameraSelector;
rectSpawningButton3ButtonComp->cameraSelector = cameraSelector;
rectSpawningButton3ButtonComp->AddOnClickListener(
std::bind(&TopScene::OnButtonClickedMember, this));
AddObject(rectSpawningButton3);
// カメラ4: バナー部分
GameObjectPtr camera4 = CreateObject("camera4");
camera4->SetLayer(4);
camera4->AddComponent<TransformComponent>();
camera4->AddComponent<Camera2DComponent>(0, 0, 1280, 720, 1280, 720,
camera4->GetLayer());
AddObject(camera4);
cameraSelector->cameras.push_back(camera4);
// 背景: カメラ4
GameObjectPtr camera4BackRect = CreateObject("camera4BackRect");
camera4BackRect->SetLayer(camera4->GetLayer());
camera4BackRect->SetOrderInLayer(-100);
camera4BackRect->AddComponent<TransformComponent>(700.f, 150.f);
camera4BackRect->AddComponent<ColliderComponent>(500.f, 40.f);
auto camera4BackRectDragComp =
camera4BackRect->AddComponent<DragComponent>();
camera4BackRect->AddComponent<Rect2DComponent>(500.f, 40.f,
GetColor(46, 53, 50));
camera4BackRect->AddComponent<TextComponent>(
"[ドラッグ可能] カメラ4", meiryo30, GetColor(200, 200, 200));
camera4BackRectDragComp->cameraSelector = cameraSelector;
AddObject(camera4BackRect);
{ camera3TransComp->SetParent(camera4BackRect); }
}
{
// 定点カメラ5
GameObjectPtr camera5 = CreateObject("camera5");
camera5->SetLayer(5);
camera5->SetOrderInLayer(-10);
camera5->AddComponent<TransformComponent>(20.f, 500.f);
camera5->AddComponent<Camera2DComponent>(0, 0, 500, 500, 200, 200,
3);
AddObject(camera5);
// 判定に入れない場合,cameraSelectorへは登録しない
cameraSelector->cameras.push_back(camera5);
GameObjectPtr caption = CreateObject("caption");
caption->SetLayer(0);
caption->SetOrderInLayer(1000);
caption->AddComponent<TransformComponent>(230.f, 680.f);
caption->AddComponent<TextComponent>("← レイヤー3 ミニマップ", meiryo20, GetColor(30, 30, 30));
AddObject(caption);
}
}
void TopScene::OnButtonClickedMember() {
int randX = GetRand(500);
int randY = GetRand(500);
int r = GetRand(256);
int g = GetRand(256);
int b = GetRand(256);
int l = GetRand(5); // == 5のみ表示されない
int oIL = GetRand(10);
GameObjectPtr randomRect = std::make_shared<GameObject>("randomRect");
randomRect->SetLayer(l);
randomRect->SetOrderInLayer(oIL);
randomRect->AddComponent<TransformComponent>(static_cast<float>(randX),
static_cast<float>(randY));
randomRect->AddComponent<ColliderComponent>(50.f, 50.f);
randomRect->AddComponent<Rect2DComponent>(50.f, 50.f, GetColor(r, g, b));
randomRect->AddComponent<AutoDestroyComponent>(10.f);
auto randomRectDragComp = randomRect->AddComponent<DragComponent>();
randomRectDragComp->cameraSelector = cameraSelector;
this->AddObject(randomRect);
GameObjectPtr randomRectChild =
std::make_shared<GameObject>("randomRectChild");
randomRectChild->SetLayer(l);
randomRectChild->SetOrderInLayer(oIL + 1);
auto randomRectChildTransComp =
randomRectChild->AddComponent<TransformComponent>(45.f, 45.f);
randomRectChild->AddComponent<Rect2DComponent>(10.f, 10.f,
GetColor(236, 154, 41));
randomRectChildTransComp->SetParent(randomRect);
this->AddObject(randomRectChild);
}
cameraSelector->cameras.push_back(camera5);
この記述がコメントアウトされていないため,左下も判定のあるキャンバスとして仕事します(cancelDraggingOnConverterNull
がFALSEの場合はクリック長押しでキャンバスを飛べない).
もしコメントアウトされた場合は,当たり判定がなくなり,コライダーのないオブジェクトと同じ振る舞いをします.
スクリーンを2つのカメラで半分に分割し,距離をあけて配置すると分かりやすいです.
ボタンコンポーネントは取り上げていませんが,コールバック関数の登録にはvoid(void)
型を採用しています.したがって,メンバ関数の登録は次のようにします.
button->AddOnClickListener(std::bind(&TopScene::OnButtonClickedMember, this));
課題
マルチスレッド化
はじめに,DXライブラリにてマルチスレッドは推奨されていません.ただし,以下のような旨が公式掲示板にて述べられていました.
1分類の関数を1つのスレッドでのみ呼ぶのであれば問題なく動作する可能性は高いと思います
ただ、あくまで『問題なく動作する可能性がある』なので、自己責任の上でご使用ください m(_ _)m
https://dxlib.xsrv.jp/cgi/patiobbs/patio.cgi?mode=view&no=4757
とのことで,レンダリング処理は流石に無理があるだろうなと感じています.
オブジェクトのアップデート処理のみならばいけるかも...?現状のコードは,次のフレームには確実に反映されている,のような書き方をしています.登録した順番に完全に依存していて,かつ表示順はこの限りではなかったので,導入しても問題ないのではと考えていました.(´・ω・`)
void Scene::Update() {
for (auto& obj : gameObjects) {
obj->Update();
}
ProcessPending();
}
検証してみたコード
void Scene::Update() {
std::vector<std::future<void>> futures;
for (auto& obj : gameObjects) {
futures.push_back(
std::async(std::launch::async, [obj]() { obj->Update(); })); // 返り値を有効にする
}
for (auto& fut : futures) {
fut.get();
}
ProcessPending();
}
↑ 完全に忘れていたテキストコンポーネント
アップデート処理で,表示内容やラップサイズの変更を監視しており,変わり次第幅計算用グラフィック関数を呼んでいました...
この監視機能はRender
に移植すると表示上問題なく動作しましたが,果たして良い選択なのかは分からないところです(未保存).
色管理
グラフィックなアプリケーションである以上,色に関して柔軟な設計が必要です.ライブラリによるRGBの変換値をそのまま代入していますが,HSBでの変数も欲しいところです.アニメーションやパーティクルをコンポーネントとして追加した時に扱いやすいですからね.
さいごに
いつもながら,ポインタは難しい.