今回は,抽象クラスまでの設計を考えたいと思います。
目的とする動作
「ボタンを置いたときにマウスカーソルを当てると,自動で色が変わり,さらにクリックをすると視覚的に状態を把握できるようにしたいなぁ。」
このマウスの操作を基準としたオブジェクトで,拡張性を持たせられるような構造を考えたいと思います。
概要
アニメーションをメインとしたオブジェクトを,描画処理・マウス処理等は既存ライブラリを利用しながら製作する。また,今回は最低限の構成ではなく,様々な入力方法に対応したものを同時に構築する。
環境
動作環境
- Windows 11
- Windows Subsystem for Linux(WSL)
仕様ライブラリ
- OpenGL(glut)
今回はglutで描画部分を構築しますが,他のライブラリといった描画に関する記述が異なる場合においても出来るだけ対応できるように考えたいと思います。
また,アイドルコールバックがある前提での構築になります。(なかった場合には途中で止まるだけです)
抽象クラスの構築
オブジェクトの元となる基底クラスを考えます。
基本的な情報として必要な情報はまず以下があげられます。
- 有効状態
- 位置
- サイズ
- マウスの接触状態
自分の中ではこれは欲しいというものを羅列してみました。この中でも位置,サイズは(x,y,z)のようなベクトル的な構造を持たせるとします。
ベクトル管理
(x,y,z)のような構造を持たせるために以下のクラスを記述します。
// Vector3D.hpp
#pragma once
namespace Obj {
struct Vector3D {
float x;
float y;
float z;
Vector3D() : x(0), y(0), z(0) {}
Vector3D(float _x, float _y) : x(_x), y(_y), z(0) {}
Vector3D(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {}
Vector3D operator+(const Vector3D& vector3D) {
return Vector3D(x + vector3D.x, y + vector3D.y, z + vector3D.z);
}
Vector3D operator-(const Vector3D& vector3D) {
return Vector3D(x - vector3D.x, y - vector3D.y, z - vector3D.z);
}
Vector3D operator*(float scalar) {
return Vector3D(x * scalar, y * scalar, z * scalar);
}
Vector3D operator/(float scalar) {
return Vector3D(x / scalar, y / scalar, z / scalar);
}
};
} // namespace Obj
こんな感じで作ってみました。ここでは単に(x,y,z)成分の定義・コンストラクタと構造体の状態における四則演算について記述しているだけです。
色管理
先ほどの記述では色をリストアップしていませんでした。色はボタンや線などによって必要な色の変数が異なるため派生先で書こうと考えていたためです。
色は今回Color255
という構造体で定義しようと思います。
必要な要素は(R,G,B,A)とし少数の場合(0.0~1.0)と整数(0~255)とライブラリによって必要な値が異なるのでこれを考慮して作っていきます。
// Color255.hpp
#pragma once
#include <stdexcept>
#include <string>
namespace Obj {
struct Color255 {
public:
union {
struct {
int r;
int g;
int b;
int a;
};
int iValue[4];
};
union {
struct {
float fr;
float fg;
float fb;
float fa;
};
float fValue[4];
};
Color255() : r(0), g(0), b(0), a(255) { CalcFloat(); }
Color255(int _grayScale, int _a = 255) // grayscale(0~255), alpha(0~255)
: r(_grayScale), g(_grayScale), b(_grayScale), a(_a) {
CalcFloat();
}
Color255(float _fgrayScale, float _fa = 1.f) // grayscale(0~1), alpha(0~1)
: fr(_fgrayScale), fg(_fgrayScale), fb(_fgrayScale), fa(_fa) {
CalcInt();
}
Color255(int _r, int _g, int _b, int _a = 255) // (r, g, b, alpha) each (0~255)
: r(_r), g(_g), b(_b), a(_a) {
CalcFloat();
}
Color255(float _fr, float _fg, float _fb, float _fa = 1.f) // (r, g, b, alpha) each (0~1)
: fr(_fr), fg(_fg), fb(_fb), fa(_fa) {
CalcInt();
}
Color255(std::string _code) // ("#00ff00"{or "00ff00"})
: a(255) {
std::string formalCode = _code;
formalCode = SplitSharp(formalCode);
int value = 0;
try {
value = std::stoi(formalCode, nullptr, 16);
} catch (const std::invalid_argument &e) {
value = 0;
} catch (const std::out_of_range &e) {
value = 0;
}
r = value / (256 * 256);
g = (value / 256) % 256;
b = value % 256;
CalcFloat();
}
Color255(std::string _code, int _a) // ("#00ff00"{or "00ff00"}, alpha) alpha:(0~255)
: a(_a) {
std::string formalCode = _code;
formalCode = SplitSharp(formalCode);
int value = 0;
try {
value = std::stoi(formalCode, nullptr, 16);
} catch (const std::invalid_argument &e) {
value = 0;
} catch (const std::out_of_range &e) {
value = 0;
}
r = value / (256 * 256);
g = (value / 256) % 256;
b = value % 256;
CalcFloat();
}
// パラメータを再計算
void CalcFloat() {
fr = (float)r / 255.f;
fg = (float)g / 255.f;
fb = (float)b / 255.f;
fa = (float)a / 255.f;
}
void CalcInt() {
r = (int)(fr * 255.f);
g = (int)(fg * 255.f);
b = (int)(fb * 255.f);
a = (int)(fa * 255.f);
}
Color255 operator*(float v) {
Color255 res = Color255(fr * v, fg * v, fb * v, fa);
CalcInt();
return res;
}
Color255 operator/(float v) {
float vInv = 1.f / v;
Color255 res = Color255(fr * vInv, fg * vInv, fb * vInv, fa);
CalcInt();
return res;
}
operator float *() { return (float *)&fr; }
operator const float *() { return (const float *)&fr; }
Color255 &operator=(const Color255 &color255) {
fr = color255.fr;
fg = color255.fg;
fb = color255.fb;
fa = color255.fa;
r = color255.r;
g = color255.g;
b = color255.b;
a = color255.a;
return (*this);
}
private:
std::string SplitSharp(std::string _code) {
std::string code = _code, result = code;
if (code.find("#") != std::string::npos) {
result = code.substr(1);
}
return result;
}
};
} // namespace Obj
このように書いてみました。
基本機能として,整数型(int
),単精度浮動小数点数型(float
)の入力からそれぞれint
,float
へ変換,更に16進数のカラーコードからint
,float
への変換を考慮しています。
SplitSharp
関数はカラーコードの入力であった場合に,#012abc
のように#
があった場合にそれを取り除く処理が行われています。
CalcInt
関数,CalcFloat
関数は
それぞれ
int
,float
へ変換
の部分にあたります。他にもグレースケールでの入力,アルファ値のデフォルト引数等にも対応しています。
入力例
Color255 a("#dafcf5");
Color255 b("#812a2b");
Color255 c(150, 50);
Color255 d(0xFF, 0x77, 0xFE);
Color255 e(.5f, .2f, .9f, .5f);
Color255 f();
-
a
: 文字入力の場合 (a:#dafcf5
) -
b
:a
で#
が無いような場合の書き方です。(b:#812a2b
) -
c
: グレースケール(不透明度: 50/255 [%]) -
d
:int
における16進数での表記 -
e
:float
における表記 -
f
: 初期値
時間管理
実装方法は様々ですが,今回は以下の方法を用います。
まずはヘッダファイル。
// Time.hpp
#pragma once
#include <chrono>
namespace Obj {
class Time {
public:
static void Initialize();
static void Update();
static double DeltaTime() { return deltaTime; }
private:
static std::chrono::system_clock::time_point previousTime;
static double deltaTime;
};
} // namespace Obj
次にソースファイル
// Time.cpp
#include "Time.hpp"
std::chrono::system_clock::time_point Obj::Time::previousTime = std::chrono::system_clock::time_point();
double Obj::Time::deltaTime = 0.;
void Obj::Time::Initialize() { previousTime = std::chrono::system_clock::now(); }
void Obj::Time::Update() {
auto nowTime = std::chrono::system_clock::now();
deltaTime = std::chrono::duration_cast<std::chrono::milliseconds>(nowTime - previousTime).count() / 1000.;
previousTime = nowTime;
}
この計測方法では環境により誤差が生じるかもしれないです。
Initialize
関数はpreviousTime
変数を初期化するために最初に1回呼び出す必要があります。
現在時刻と前回時刻の経過時間の差(deltaTime
)を計算し、次の呼び出しのために previousTime
を現在時刻に更新して待機します。
抽象クラス (Object2D)
ここで先に抽象クラスを考えてみたものを挙げます。少し長文になりますが,最初にヘッダーとソースを記載します。
// Object2D.hpp
#pragma once
#include <vector>
#include "AnimationPointerData.hpp"
#include "ApplicationPreference.hpp"
#include "Color255.hpp"
#include "ColorTypes.hpp"
#include "MouseInput.hpp"
#include "Time.hpp"
#include "Vector3D.hpp"
#include "VectorTypes.hpp"
namespace Obj {
class Object2D {
public:
Object2D(Vector3D _pos, Vector3D _size)
: pos(_pos),
size(_size),
enabled(true),
mouseHit(false),
mouseClicked(false),
mouseSelected(false),
beCalledNoMouse(false),
enforcedCollision(1),
expandedNum(false),
enforcedHovered(false),
enforcedClicked(false),
enforcedSelected(false) {
}
virtual ~Object2D();
protected:
void UpdateEnforcedMouseCollision();
// アニメーションの更新を取りまとめる
void UpdateAnimations() {
UpdateAnimationColor(&innerAnimation);
UpdateAnimationColor(&outerAnimation);
UpdateAnimation(&innerAlphaAnimation);
UpdateAnimation(&outerAlphaAnimation);
UpdatePointerAnimation();
}
// アニメーションカラー初期設定
void SetAnimationColorPoint(AnimationColorStatus* type, Color255 _start, Color255 _goal);
// アニメーション初期設定
void SetAnimationPoint(AnimationStatus* type, float _start, float _goal);
// 当たり判定が矩形の場合はこれを用いることを推奨する
void CollideMouseAsBox();
virtual void CollideMouse() = 0; // マウス判定処理
Vector3D pos; // 頂点座標
Vector3D size; // 大きさ
bool enabled; // 有効状態(false時は判定,更新処理が基本ない)
bool mouseHit; // マウスホバー時
bool mouseSelected; // マウスがアクティブな時
bool mouseClicked; // マウスがオブジェクト上でクリックされた時
bool beCalledNoMouse;
bool expandedNum; // 当たり判定をクリック時に拡張するか
AnimationColorStatus innerAnimation; // 内側色アニメーション構造体
AnimationColorStatus outerAnimation; // 外側色アニメーション構造体
AnimationStatus innerAlphaAnimation; // 内側透過アニメーション構造体
AnimationStatus outerAlphaAnimation; // 外側透過アニメーション構造体
std::vector<Object2D*> children; // 子オブジェクト(追従などが可能になる)
int enforcedCollision; // 当たり判定で優先順位をつける場合,これを用いる(乱用禁止)
std::string tag;
private:
// SetAnimationColorPoint()で設定された値の更新処理(推奨呼び出し)
void UpdateAnimationColor(AnimationColorStatus* type);
// pColorAnimation,
// pAnimationに追加された(座標系等々)のパラメータの更新処理(推奨呼び出し)
void UpdatePointerAnimation();
// SetAnimationPoint()で設定された値の更新処理(推奨呼び出し)
void UpdateAnimation(AnimationStatus* type);
std::vector<AnimationColorPointer> pColorAnimation;
std::vector<AnimationPointer> pAnimation;
std::vector<AnimationPointerInt> pAnimationInt;
bool enforcedHovered;
bool enforcedClicked;
bool enforcedSelected;
public:
virtual void Update() = 0;
virtual void Draw() = 0;
virtual void Collide() = 0;
Vector3D GetPos() { return pos; }
Vector3D GetSize() { return size; }
// オブジェクト移動系(子要素含む)
virtual bool Move(Vector3D _delta, bool _involvedParent = true);
// 親(自分)のみ移動(絶対値)
bool SetPos(Vector3D _pos) {
pos = _pos;
return true;
}
// 大きさ設定
bool SetSize(Vector3D _size) {
size = _size;
return true;
}
Vector3D* GetVectorPointer(VectorType type) {
switch (type) {
case VectorType::POS:
return &pos;
case VectorType::SIZE:
return &size;
default:
return nullptr;
}
}
bool SetEnabled(bool _enabled) {
enabled = _enabled;
return true;
}
bool SetEnabled() { return enabled; }
// 上にレイヤが重なっていた場合の解除処理
bool SetNoMouse() {
mouseHit = false;
return true;
}
bool SetNoMouseWithClick() {
mouseHit = false;
mouseClicked = false;
mouseSelected = false;
return true;
}
// マウス入力解除(Selectedなどの任意の分岐後に呼び出し等)
bool SetMouseOff() {
mouseClicked = false;
mouseSelected = false;
return true;
}
// マウス判定系取得
bool GetMouseHit() { return mouseHit; }
bool GetMouseClicked() { return mouseClicked; }
bool GetMouseSelected() { return mouseSelected; }
void SetMouseHit(bool flag) { enforcedHovered = flag; }
void SetMouseClicked(bool flag) { enforcedClicked = flag; }
void SetMouseSelected(bool flag) { enforcedSelected = flag; }
void ChangeColorWithAnimation(Color255* pColor, Color255* endColor, float duration);
void ChangeValueWithAnimation(float* pValue, float endValue, float duration);
void ChangeValueWithAnimation(int* pValue, int endValue, float duration);
void SetEnforcedCollision(int _enforcedCollision = 1) {
enforcedCollision = _enforcedCollision;
}
int GetEnforcedCollision() {
return enforcedCollision;
}
// アニメーション設定
bool SetInnerAnimation(float _duration) {
innerAnimation.animationEnabled = true;
innerAnimation.duration = _duration;
innerAnimation.durationRemain = _duration;
innerAnimation.elapsedTime = 0.f;
innerAlphaAnimation.animationEnabled = true;
innerAlphaAnimation.duration = _duration;
innerAlphaAnimation.durationRemain = _duration;
innerAlphaAnimation.elapsedTime = 0.f;
return true;
}
// アニメーション設定
bool SetInnerAnimation() {
innerAnimation.animationEnabled = false;
innerAlphaAnimation.animationEnabled = false;
return true;
}
// アニメーション設定
bool SetOuterAnimation(float _duration) {
outerAnimation.animationEnabled = true;
outerAnimation.duration = _duration;
outerAnimation.durationRemain = _duration;
outerAnimation.elapsedTime = 0.f;
outerAlphaAnimation.animationEnabled = true;
outerAlphaAnimation.duration = _duration;
outerAlphaAnimation.durationRemain = _duration;
outerAlphaAnimation.elapsedTime = 0.f;
return true;
}
// アニメーション設定
bool SetOuterAnimation() {
outerAnimation.animationEnabled = false;
outerAlphaAnimation.animationEnabled = false;
return true;
}
// 子要素登録
virtual bool RegisterChildren(Object2D* _object);
virtual bool DeleteAllChildren() {
children.clear();
return true;
}
// 任意でタグをつけることが出来る
void SetTag(std::string _tag) { tag = _tag; }
std::string GetTag() { return tag; }
};
} // namespace Obj
次にソースファイル
// Object2D.cpp
#include "Object2D.hpp"
Obj::Object2D::~Object2D() {}
// 強制的に当たり判定を変更する場合,非推奨
void Obj::Object2D::UpdateEnforcedMouseCollision() {
if (enforcedHovered) {
enforcedHovered = false;
mouseHit = true;
}
if (enforcedClicked) {
enforcedClicked = false;
mouseClicked = true;
}
if (enforcedSelected) {
enforcedClicked = false;
mouseSelected = true;
}
}
// カラーアニメーションの値の設定処理
void Obj::Object2D::SetAnimationColorPoint(AnimationColorStatus* type, Color255 _start, Color255 _goal) {
type->elapsedTime = 0.f;
type->start = _start;
if (type->end.r != _goal.r &&
type->end.g != _goal.g &&
type->end.b != _goal.b)
type->durationRemain = type->duration;
type->end = _goal;
type->current = _start;
if (type->durationRemain <= 0.f || !type->animationEnabled) {
type->current = type->end;
return;
}
type->m[0] = (type->end.r - type->start.r) / type->durationRemain;
type->m[1] = (type->end.g - type->start.g) / type->durationRemain;
type->m[2] = (type->end.b - type->start.b) / type->durationRemain;
}
// カラーアニメーションの値の更新処理
void Obj::Object2D::UpdateAnimationColor(AnimationColorStatus* type) {
if (type->animationEnabled) {
type->durationRemain -= Time::DeltaTime();
type->elapsedTime += Time::DeltaTime();
if (type->elapsedTime >= type->duration || type->durationRemain <= 0.f) {
type->elapsedTime = type->duration;
type->durationRemain = type->duration;
} else {
type->current =
Color255((int)(type->m[0] * type->elapsedTime + type->start.r),
(int)(type->m[1] * type->elapsedTime + type->start.g),
(int)(type->m[2] * type->elapsedTime + type->start.b));
}
}
}
// アニメーションの値の設定処理
void Obj::Object2D::SetAnimationPoint(AnimationStatus* type, float _start, float _goal) {
type->elapsedTime = 0.f;
type->start = _start;
if (type->end != _goal) {
type->durationRemain = type->duration;
}
type->end = _goal;
type->current = _start;
if (type->durationRemain <= 0.f || !type->animationEnabled) {
type->current = type->end;
return;
}
type->m = (type->end - type->start) / type->durationRemain;
}
// アニメーションの値の更新処理
void Obj::Object2D::UpdateAnimation(AnimationStatus* type) {
if (type->animationEnabled) {
type->durationRemain -= Time::DeltaTime();
type->elapsedTime += Time::DeltaTime();
if (type->elapsedTime >= type->duration || type->durationRemain <= 0.f) {
type->elapsedTime = type->duration;
type->current += type->m * type->elapsedTime;
} else {
type->current += type->m * type->elapsedTime;
}
if (type->m < 0 && type->current <= type->end)
type->current = type->end;
else if (type->m > 0 && type->current >= type->end)
type->current = type->end;
}
}
// ChangeValueにより登録された状態変数の更新
void Obj::Object2D::UpdatePointerAnimation() {
for (auto it = pColorAnimation.begin(); it != pColorAnimation.end(); it++) {
UpdateAnimationColor(&(it->animation));
*(it->color) = it->animation.current;
}
for (auto it = pAnimation.begin(); it != pAnimation.end(); it++) {
UpdateAnimation(&(it->animation));
*(it->value) = it->animation.current;
}
for (auto it = pAnimationInt.begin(); it != pAnimationInt.end(); it++) {
UpdateAnimation(&(it->animation));
*(it->value) = (int)it->animation.current;
}
}
// 四角形オブジェクトの場合に使用
void Obj::Object2D::CollideMouseAsBox() {
if (!enabled) return;
bool beforeMouseClicked = mouseClicked;
bool goSelecting = false;
Vector3D offset = Vector3D();
bool clickExpanded =
Input::MouseInput::GetClick(GLUT_LEFT_BUTTON) >= PressFrame::FIRST ? true : false;
// オブジェクトの当たり判定を拡張するか
if (clickExpanded && expandedNum && mouseClicked) {
offset.x = ApplicationPreference::windowSize.x / 6.f;
offset.y = ApplicationPreference::windowSize.y / 6.f;
}
// マウスがオブジェクトの拡張した領域も含め範囲内か
if (pos.x - offset.x <= Input::MouseInput::GetMouse().x &&
pos.x + size.x + offset.x >= Input::MouseInput::GetMouse().x &&
pos.y - offset.y <= Input::MouseInput::GetMouse().y &&
pos.y + size.y + offset.y >= Input::MouseInput::GetMouse().y) {
mouseHit = true;
if (Input::MouseInput::GetClick(GLUT_LEFT_BUTTON) >= PressFrame::FIRST) {
if (Input::MouseInput::GetClick(GLUT_LEFT_BUTTON) == PressFrame::FIRST)
mouseClicked = true;
} else {
// マウスがオブジェクトのオリジナルの領域内か
if (pos.x <= Input::MouseInput::GetMouse().x &&
pos.x + size.x >= Input::MouseInput::GetMouse().x &&
pos.y <= Input::MouseInput::GetMouse().y &&
pos.y + size.y >= Input::MouseInput::GetMouse().y) {
mouseClicked = false;
goSelecting = true;
}
}
} else {
mouseHit = false;
if (Input::MouseInput::GetClick(GLUT_LEFT_BUTTON) >= PressFrame::FIRST && !mouseClicked) {
mouseSelected = false;
}
}
if (Input::MouseInput::GetClick(GLUT_LEFT_BUTTON) == PressFrame::ZERO)
mouseClicked = false;
if (beforeMouseClicked && !mouseClicked && goSelecting) {
mouseSelected = true;
}
beCalledNoMouse = false;
}
// // 色構造体変数のアニメーション設定
void Obj::Object2D::ChangeColorWithAnimation(Color255* pColor, Color255* endColor, float duration) {
for (auto it = pColorAnimation.begin(); it != pColorAnimation.end(); it++) {
if (it->color == pColor) {
if (it->animation.end.r == endColor->r &&
it->animation.end.g == endColor->g &&
it->animation.end.b == endColor->b)
return;
else {
pColorAnimation.erase(it);
break;
}
}
}
AnimationColorPointer* p = new AnimationColorPointer(AnimationColorStatus(), pColor);
p->animation.animationEnabled = true;
p->animation.duration = duration;
p->animation.durationRemain = duration;
p->animation.elapsedTime = 0.f;
SetAnimationColorPoint(&(p->animation), *pColor, *endColor);
pColorAnimation.push_back(*p);
}
// Float型変数のアニメーション設定
void Obj::Object2D::ChangeValueWithAnimation(float* pValue, float endValue, float duration) {
for (auto it = pAnimation.begin(); it != pAnimation.end(); it++) {
if (it->value == pValue) {
if ((int)(it->animation.end) == (int)endValue)
return;
else {
pAnimation.erase(it);
break;
}
}
}
AnimationPointer* p = new AnimationPointer(AnimationStatus(), pValue);
p->animation.animationEnabled = true;
p->animation.duration = duration;
p->animation.durationRemain = duration;
p->animation.elapsedTime = 0.f;
SetAnimationPoint(&(p->animation), *pValue, endValue);
pAnimation.push_back(*p);
}
// Int型変数のアニメーション設定
void Obj::Object2D::ChangeValueWithAnimation(int* pValue, int endValue, float duration) {
for (auto it = pAnimationInt.begin(); it != pAnimationInt.end(); it++) {
if (it->value == pValue) {
if ((int)(it->animation.end) == (int)endValue)
return;
else {
pAnimationInt.erase(it);
break;
}
}
}
AnimationPointerInt* p = new AnimationPointerInt(AnimationStatus(), pValue);
p->animation.animationEnabled = true;
p->animation.duration = duration;
p->animation.durationRemain = duration;
p->animation.elapsedTime = 0.f;
SetAnimationPoint(&(p->animation), (float)*pValue, (float)endValue);
pAnimationInt.push_back(*p);
}
// オブジェクトの全体的な移動
bool Obj::Object2D::Move(Vector3D _delta, bool _involvedParent) {
if (_involvedParent) pos = Vector3D(pos.x + _delta.x, pos.y + _delta.y);
// 子要素にも適用
for (int i = 0; i < children.size(); i++) {
children[i]->Move(_delta);
}
return true;
}
// 子要素の登録
bool Obj::Object2D::RegisterChildren(Object2D* _object) {
for (auto& item : children) {
if (item == _object) return false;
}
children.insert(children.end(), _object);
return true;
}
このようになります。ここからメソッドごとに紹介します。(順番はヘッダ通りではないです。)
Update
関数
これはアイドルコールバック時の処理としてその都度呼ばれる処理となります。内部の機能として今回のメインであるアニメーションなどの更新処理をここでまとめることにより実現しようとしています。しかし,今回はvirtual
で定義しており,派生先で実際の処理は書いていくことになります。
Draw
関数
これは上と同様です。しかし,ライブラリによってオブジェクトの描画方法は異なってくるので派生先ではここを慎重に考える必要があります。
Collide
関数
マウスの判定処理はこの関数の中で記述されます。基本はpos
とpos+size
で構成される四角形を想定しています。そのような場合には,クラス内にあるCollideMouseAsBox
関数を派生先で呼ぶことでこの機能を果たすことが可能です。
CollideMouseAsBox
関数
これは四角形であった場合を想定した当たり判定の処理となります。まずはもう一度その部分のソース記述を記載します。
void Obj::Object2D::CollideMouseAsBox() {
if (!enabled) return;
bool beforeMouseClicked = mouseClicked;
bool goSelecting = false;
Vector3D offset = Vector3D();
bool clickExpanded =
Input::MouseInput::GetClick(GLUT_LEFT_BUTTON) >= PressFrame::FIRST ? true : false;
if (clickExpanded && expandedNum && mouseClicked) {
offset.x = ApplicationPreference::windowSize.x / 6.f;
offset.y = ApplicationPreference::windowSize.y / 6.f;
}
if (pos.x - offset.x <= Input::MouseInput::GetMouse().x &&
pos.x + size.x + offset.x >= Input::MouseInput::GetMouse().x &&
pos.y - offset.y <= Input::MouseInput::GetMouse().y &&
pos.y + size.y + offset.y >= Input::MouseInput::GetMouse().y) {
mouseHit = true;
if (Input::MouseInput::GetClick(GLUT_LEFT_BUTTON) >= PressFrame::FIRST) {
if (Input::MouseInput::GetClick(GLUT_LEFT_BUTTON) == PressFrame::FIRST)
mouseClicked = true;
} else {
if (pos.x <= Input::MouseInput::GetMouse().x &&
pos.x + size.x >= Input::MouseInput::GetMouse().x &&
pos.y <= Input::MouseInput::GetMouse().y &&
pos.y + size.y >= Input::MouseInput::GetMouse().y) {
mouseClicked = false;
goSelecting = true;
}
}
} else {
mouseHit = false;
if (Input::MouseInput::GetClick(GLUT_LEFT_BUTTON) >= PressFrame::FIRST && !mouseClicked) {
mouseSelected = false;
}
}
if (Input::MouseInput::GetClick(GLUT_LEFT_BUTTON) == PressFrame::ZERO)
mouseClicked = false;
if (beforeMouseClicked && !mouseClicked && goSelecting) {
mouseSelected = true;
}
beCalledNoMouse = false;
}
まず,オブジェクトが有効状態でない場合には判定をしません。基本的にはマウスの座標がオブジェクトの座標の内側に居るかを判定し,True
の場合にはフラグを更新するというものです。
- オブジェクトの範囲内であればHit状態にする。
- オブジェクトの範囲内で1フレーム目の(左)クリックがあった場合はClickedにする。
- オブジェクトの範囲内でマウスのクリックが外れ,Clickedであった場合はSelectedにする。
また,offset
を使ったマウス座標を基にした分岐がありますが,これはスライダーといったオブジェクトにおいて,少々範囲外でもマウスの移動動作が反映されるように,特定のオブジェクトの値を設定しておくことで作動します。これによりmouseClicked
がTrue
の時に当たり判定が拡大するようになります。
複数のオブジェクトが重なった際に重複判定を対策する場合は,以下の補足記事を参照してください (更新:2023/3/11)
Pos
関連,Size
関連
Vector3D GetPos();
Vector3D GetSize();
// オブジェクト移動系(子要素含む)
virtual bool Move(Vector3D _delta, bool _involvedParent = true);
// 親(自分)のみ移動(絶対値)
bool SetPos(Vector3D _pos);
// 大きさ設定
bool SetSize(Vector3D _size);
Vector3D* GetVectorPointer(VectorType type);
以上のような関数が該当します。
これらは,座標・大きさを指定,または取得するための関数となります。(ゲッタ・セッタ)
Move
関数は自分と自分の下にいるオブジェクト(子要素)全てに対して,引数のベクトル分だけ移動を行います。
ちなみに,GetVectorPointer
関数というものがありポインタを返してしまっているのですが,これはアニメーションのために使われるものです。(ゲッタ・セッタの概念の崩壊)
→(改善の余地あり)
フラグ関連
-
bool SetEnabled(bool _enabled)
: 有効状態の設定 -
bool GetEnabled()
: 有効状態の取得 -
bool SetMouseOff()
: マウス解除(オブジェクトがSelected
になった場合に有効) -
void SetMouseHit(bool flag)
: 強制的にマウスのフラグを変更する -
void SetMouseClicked(bool flag)
: 強制的にマウスのフラグを変更する -
void SetMouseSelected(bool flag)
: 強制的にマウスのフラグを変更する
アニメーション関連
ここがタイトルの通り,メインの記述となる部分です。予め申すと,多少計算がおかしい部分があるかもしれません。ご了承ください。
-
void ChangeColorWithAnimation(Color255* pColor, Color255* endColor, float duration)
void ChangeValueWithAnimation(float* pValue, float endValue, float duration)
void ChangeValueWithAnimation(int* pValue, int endValue, float duration)
第1引数: アニメーションを行いたい変数のポインタ
第2引数: 最終的な値
第3引数: 所要時間
となります。
アニメーションは1次関数のように遷移します。
(現状のシステムはChangeValueWithAnimation
において引数として入力した遷移時間と実際の遷移時間が大幅に違うので改善する必要があります。約10倍)
また,Empty
オブジェクトのようなものを定義して特別な機能を持たせないことで,外部変数のアニメーションにも流用することが出来るかと思います。(ex:3Dオブジェクトのパラメータのアニメーション,タイマー)
ChangeColorWithAnimation
関数の場合のソースを以下に示します。
void Obj::Object2D::ChangeColorWithAnimation(Color255* pColor, Color255* endColor, float duration) {
for (auto it = pColorAnimation.begin(); it != pColorAnimation.end(); it++) {
if (it->color == pColor) {
if (it->animation.end.r == endColor->r &&
it->animation.end.g == endColor->g &&
it->animation.end.b == endColor->b)
return;
else {
pColorAnimation.erase(it);
break;
}
}
}
AnimationColorPointer* p = new AnimationColorPointer(AnimationColorStatus(), pColor);
p->animation.animationEnabled = true;
p->animation.duration = duration;
p->animation.durationRemain = duration;
p->animation.elapsedTime = 0.f;
SetAnimationColorPoint(&(p->animation), *pColor, *endColor);
pColorAnimation.push_back(*p);
}
受け取ったポインタがもし既に登録されていた場合,目標値が同じであるときは処理が行われません。これは,アイドルコールバックによりループ処理の中で呼ばれることが推測されるためこのようになります。目標値が異なる場合は上書きという形で置き換わり,引数の情報を基にアニメーションに必要なパラメータを登録していきます。また同時に可変長配列へ追加します。
-
UpdatePointerAnimation()
: 上の3つの関数で登録された遷移はこの関数が呼び出されることで実際に動作します。 -
bool SetInnerAnimation(float _duration)
bool SetOuterAnimation(float _duration)
これは,アニメーションの遷移時間を引数に入力することでHit
からClicked
,Clicked
からSelected
と状態が変化した際に,自然にアニメーション(色遷移)が起こるようにします。 -
void SetAnimationColorPoint(AnimationColorStatus* type, Color255 _start, Color255 _goal)
void SetAnimationPoint(AnimationStatus* type, float _start, float _goal)
void Obj::Object2D::SetAnimationColorPoint(AnimationColorStatus* type, Color255 _start, Color255 _goal) {
type->elapsedTime = 0.f;
type->start = _start;
if (type->end.r != _goal.r &&
type->end.g != _goal.g &&
type->end.b != _goal.b)
type->durationRemain = type->duration;
type->end = _goal;
type->current = _start;
if (type->durationRemain <= 0.f || !type->animationEnabled) {
type->current = type->end;
return;
}
type->m[0] = (type->end.r - type->start.r) / type->durationRemain;
type->m[1] = (type->end.g - type->start.g) / type->durationRemain;
type->m[2] = (type->end.b - type->start.b) / type->durationRemain;
}
これはオブジェクトの状態を基に更新する場合の関数です。初期値と目標値の差から残り時間を除算することでオブジェクトの状態が途中で変化しても対応することが出来ます。
-
void UpdateAnimationColor(AnimationColorStatus* type)
void UpdateAnimation(AnimationStatus* type)
void Obj::Object2D::UpdateAnimationColor(AnimationColorStatus* type) {
if (type->animationEnabled) {
type->durationRemain -= Time::DeltaTime();
type->elapsedTime += Time::DeltaTime();
if (type->elapsedTime >= type->duration || type->durationRemain <= 0.f) {
type->elapsedTime = type->duration;
type->durationRemain = type->duration;
} else {
type->current =
Color255((int)(type->m[0] * type->elapsedTime + type->start.r),
(int)(type->m[1] * type->elapsedTime + type->start.g),
(int)(type->m[2] * type->elapsedTime + type->start.b));
}
}
}
SetAnimation~
関数で構築された変数を引数にすることで1次関数のような動作を実際に働かせることが出来ます。また,時間経過により特定の時間を超えた場合には目標値に固定するようにすることで,本来の値から外れないようにしています。
抽象クラスに付随する構造体等
Object2D
クラス以前でまだ記述していないものについて示します。
アニメーションの状態管理
先ほどまでのアニメーションに関する比例係数や経過時間,目標値などを格納する構造体です。
// AnimationStatus.hpp
#pragma once
#include <array>
#include "Color255.hpp"
namespace Obj {
struct AnimationColorStatus {
bool animationEnabled;
Color255 start;
Color255 end;
Color255 current;
std::array<float, 3> m;
float duration;
float durationRemain;
float elapsedTime;
float currentlyRate;
inline AnimationColorStatus()
: animationEnabled(false),
current(0),
m({}),
duration(0.f),
durationRemain(0.f),
elapsedTime(0.f),
currentlyRate(0.f),
start(0),
end(0) {}
};
struct AnimationStatus {
bool animationEnabled;
float start;
float end;
float current;
float m;
float duration;
float durationRemain;
float elapsedTime;
float currentlyRate;
inline AnimationStatus()
: animationEnabled(false),
current(0.f),
m(0.f),
duration(0.f),
durationRemain(0.f),
elapsedTime(0.f),
currentlyRate(0.f),
start(0.f),
end(0.f) {}
};
} // namespace Obj
これ以降もint
やColor255
等は分けて記述します。(今回はint
とfloat
の分割にクラステンプレートは用いませんでした。)
アニメーションの状態・ポインター管理
先ほどの状態に加え,ポインタを格納します。
// AnimationPointerData.hpp
#pragma once
#include "AnimationStatus.hpp"
namespace Obj {
struct AnimationColorPointer {
AnimationColorStatus animation;
Color255* color;
inline AnimationColorPointer() : animation(), color(nullptr) {}
inline AnimationColorPointer(AnimationColorStatus _status, Color255* _color)
: animation(_status), color(_color) {}
};
struct AnimationPointer {
AnimationStatus animation;
float* value;
inline AnimationPointer() : animation(), value(nullptr) {}
inline AnimationPointer(AnimationStatus _status, float* _value)
: animation(_status), value(_value) {}
};
struct AnimationPointerInt {
AnimationStatus animation;
int* value;
inline AnimationPointerInt() : animation(), value(nullptr) {}
inline AnimationPointerInt(AnimationStatus _status, int* _value)
: animation(_status), value(_value) {}
};
} // namespace Obj
これらでアニメーションの状態を常に更新していきます。
色の状態列挙
// ColorTypes.hpp
#pragma once
enum class ColorType {
INNER,
HOVERED,
CLICKED,
SELECTED,
OUTER_INNER,
OUTER_HOVERED,
OUTER_CLICKED,
OUTER_SELECTED
};
内部・外部の色を引き出すための関数にて,判定をする際のswitch
文に用いるものです。
マウスクリックの状態列挙
// PressFrameEnum.hpp
#pragma once
enum PressFrame {
RELEASE,
ZERO,
FIRST,
MUCH,
NUM,
};
この列挙を大小比較することで,長押しや1フレームのみの検出等にも応用可能です。
ベクトルの種類列挙
#pragma once
enum class VectorType { POS, SIZE };
マウスの状態判定・管理
マウスにおける情報の提供方法は使用するライブラリによって異なります。glut
はコールバックで得られますし,他のライブラリでは関数により得ることが出来ることもあります。今回はglut
の場合における方法を記述します。(他のライブラリではUpdate
関数を書き換える,またはコールバックから特定の関数を呼んであげる必要があります。)
まずはヘッダファイル
// MouseInput.hpp
#pragma once
#include <GL/glut.h>
#include <stdio.h>
#include "MouseClickType.hpp"
#include "PressFrameEnum.hpp"
#include "Vector3D.hpp"
// 併用するライブラリごとにUpdateの変更する必要
namespace Input {
class MouseInput {
public:
static void Update();
static void UpdateMouseState(int _button, int _state); // コールバックで取得する場合に使用
static void SetMousePos(Obj::Vector3D _pos) { mouse = _pos; } // コールバックで取得する場合に使用
static Obj::Vector3D GetMouse() { return mouse; }
static Obj::Vector3D GetMouseWindow() { return mouseWin; }
static int GetClick(int _button);
static float GetWheelRot() { return wheelRot; }
private:
static void UpdateClick(int* _value, int* _andValue);
private:
static float wheelRot;
static Obj::Vector3D mouse;
static Obj::Vector3D mouseWin;
static MouseClickType mouseClickType;
static MouseClickType mouseFirstQueue;
};
} // namespace Input
基本的にクリックを取得する場合はGetClick
関数を用い,ライブラリにおける定数等を引数に代入することで目的のPressFrame
が得られるようにします。
次にソースファイル
// MouseInput.cpp
#include "MouseInput.hpp"
Obj::Vector3D Input::MouseInput::mouse = Obj::Vector3D();
Obj::Vector3D Input::MouseInput::mouseWin = Obj::Vector3D();
MouseClickType Input::MouseInput::mouseClickType = MouseClickType();
MouseClickType Input::MouseInput::mouseFirstQueue = MouseClickType();
float Input::MouseInput::wheelRot = 0;
void Input::MouseInput::Update() {
UpdateClick(&mouseClickType.left, &mouseFirstQueue.left);
UpdateClick(&mouseClickType.right, &mouseFirstQueue.right);
UpdateClick(&mouseClickType.middle, &mouseFirstQueue.middle);
// FIXME: wheelRot
}
// ライブラリによってSwitch分岐の値が異なる
int Input::MouseInput::GetClick(int _button) {
switch (_button)
{
case GLUT_LEFT_BUTTON:
return mouseClickType.left;
case GLUT_RIGHT_BUTTON:
return mouseClickType.right;
case GLUT_MIDDLE_BUTTON:
return mouseClickType.middle;
default:
return -1;
}
return -1;
}
// ボタンの押し込み状態の更新
void Input::MouseInput::UpdateClick(int* _value, int* _andValue) {
if (*_value >= PressFrame::FIRST) {
*_value = PressFrame::MUCH;
}
if (*_andValue == PressFrame::FIRST) {
*_value = PressFrame::FIRST;
*_andValue = PressFrame::ZERO;
} else if (*_andValue == PressFrame::RELEASE) {
*_value = PressFrame::ZERO;
*_andValue = PressFrame::ZERO;
}
}
// コールバックで特定のマウスボタンの処理に変更があった場合
void Input::MouseInput::UpdateMouseState(int _button, int _state) {
int state;
switch (_state) {
case GLUT_DOWN:
state = PressFrame::FIRST;
break;
case GLUT_UP:
state = PressFrame::RELEASE;
break;
default:
break;
}
switch (_button) {
case GLUT_LEFT_BUTTON:
mouseFirstQueue.left = state;
break;
case GLUT_RIGHT_BUTTON:
mouseFirstQueue.right = state;
break;
case GLUT_MIDDLE_BUTTON:
mouseFirstQueue.middle = state;
break;
}
}
このクラスは常にループ処理として呼ばれていることを前提としています。UpdateClick
関数により,それぞれのマウスクリックに対してどのフレームの状態かを更新するようにしています。尚,今回はホイールの回転量について記述していません。
また,Switch文にライブラリの定数を用いることで,ライブラリによって提供される値をそのまま使うことが出来るようになります。
ここでMouseClickType
構造体は以下になります。
// MouseClickType.hpp
#pragma once
struct MouseClickType {
int left;
int right;
int middle;
int mouse4;
int mouse5;
MouseClickType() : left(0), right(0), middle(), mouse4(0), mouse5(0) {}
};
それぞれのボタンごとにint
型を定義し,状態を格納するようにします。
まとめ: I
オブジェクトの共通部分を記述し,派生先で効率よく記述できるように試みた。名前空間を用い,できるだけ衝突が起こらないようにしなければならない。