0.軽く自己紹介
初めましての方は初めまして!owapote(おわぽて)といいます。
DxLib歴は実質3ヶ月くらいです。プログラミング自体は7~8年やっていますが...
現在、DxLibで2Dのシューティングゲームを制作中!
https://x.com/owapote_pro/status/1704535155434791169?s=20
恐れ多いことですが、また機会があったらその辺りの記事を書きたいと思います。
ただ、こういった記事を書くのは初めてなので、有識者の方々ぜひご教示ください。
1.はじめに
私がDxLibを触って1ヶ月くらいの感想、
DxLibにVector2Dがない!?
DxLibにはVECTOR(ここではVector3Dと表記)以降しかありませんでした。要は2次元ベクトルがない!
毎回z=0.0fとか0でVector3Dを使えば良いんですけど、毎回それを書くのも煩わしいので「どうせなら自作したい!」という気持ちになり、作っちゃいました。
ただ、他のライブラリーには二次元ベクトル系統がデフォルトで存在する場合があります。そういう場合は作らなくて問題ありません。
DirectXMathを見ながら作成したものになります。ライブラリーとまでは言えないかもしれませんが、初めて作ったので我ながらお気に入りです。
2.Vector2Dを作る狙いやメリット
狙い
- DxLib用に使いやすくする
- 関数へのwrapなど(続編で解説予定です)
- ないものは自作すると楽になるよというお話
- テンプレートの便利さをついでに知ってもらう
メリット
- 各要素ごとでちまちま計算せずに済む
- ↑これのお陰で可読性が向上する
- C++の理解が進む(副作用)
- 基礎的なベクトル操作ができるようになる(決して某一方通行さんではない)
- 二次元座標が扱いやすくなる
3.class?struct?
閑話休題、作っていこうと思います。
...が、classで作るかstructで作るか悩みました。
classは基本private、structはpublicです。
classで作るメリットがそこまでなさそうだったのと、個人的に「こういうステータスみたいなやつはstructで作りたい!」という気持ちがあったので今回はstructで作ります。
隠蔽のメリットがあまりないと感じたので、まぁ良いでしょう。
4.パパっとVector2Dを作っていく
サクッとVector2Dのstructを作っていきます。ついでにコンストラクタを添えて。
struct Vector2D {
float x;
float y;
//デフォルトコンストラクタ
Vector2D() noexcept = default;
constexpr Vector2D(const float &x,const float &y) noexcept: x(x), y(y) {};
explicit constexpr Vector2D(const float &f) noexcept: x(f), y(f) {};
}
デフォルトコンストラクタと非静的メンバ変数の初期化×2で良いでしょう。
C++のstruct、実はメンバー関数とかコンストラクタも作れるんですよね。それなら別にclassだけでもええやん
まあクラスへのアクセスは面倒だから隠蔽しなくても良いモノならstructの方が作りやすいかとは思います。
デフォルトコンストラクタで0とか入れても良いんですけど、可読性が良いのでdefaultにしてみました。合っているんですかね...?
まあその他2つは見たまんまですね。
4-1.キーワードについて
先程noexceptやexplicitやconstexprなどの単語が出てきました。
これはキーワードと言って、予約語としてC++のライブラリーにあります。
例えば、外部ファイルで関数の中身を定義している(extern)とか、継承の禁止(final)とか、そういうのがキーワードとしてあります。
どれも演算子のオーバーロード辺りで多用しています。
ただ、キーワードってかなり大量にあるので、今回使うキーワードだけを抜粋してざっくり解説していきますね!今回の記事内での重要度を5段階評価で付けます。
noexcept 重要度:★★★★★
これを関数の末尾に付けることで、「例外を投げない」ことが保証されます。
今回の記事で頻繁に出てくる、かなり重要なキーワードです。
逆に、例外を投げた瞬間にエラーをはきます。
例えばゼロ除算だったり、平方根の中が負の値になったりする関数にはnoexceptは付けないでください。
本当に例外を投げないのか、きちんと確認することが大事です。
const 重要度:★★★★★
参照渡しです。
簡単に言えば「引数の値はこの関数内では変更できない」ということが保証されます。
今回に限らず、どの関数を作っていても結構頻繁に出てくるのでぜひ覚えましょう。
constexpr 重要度:★★★☆☆
「コンパイル時にもう定数式として使える」ようになります。ただし、条件がちょっと厳しめなのでこちらで確認をしておくと良いでしょう。
使い方次第で実行時のパフォーマンス爆上がりらしいですが、今のところ実感はないですね。
←そんなに大きいプログラムを作っていないからね
inline 重要度:★★☆☆☆
「inlineとつけた関数の中身をコンパイル時にプログラム上へ展開」します。ただし、関数の中身が簡単であることが条件です。中身が複雑だとinline展開がされない可能性があります。
ループ内などで関数を毎回呼び出すのはコストがかかります。
例えばとある漢字が読めない時、「その都度QuizKn○ckの山本さんを電話で呼び出すのか」「その都度机の上にある漢字辞典で調べるのか」の2択だったら後者の方がコストは低いですね。
その代わりビルドの時間が少しだけ長くなるかも?
explicit 重要度:★☆☆☆☆
「暗黙の型変換を禁止」させます。
今回はコンストラクタの部分で使用しています。
これを付けないと引数のfloatがVector2Dに変換されてしまう可能性があるらしい...
以上です。この辺りをざっくり抑えておくとこれ以降のプログラムの理解につながると思います。
4-2.Templateについて
さて、早速演算子のお話をしていきたいのですが、1つ問題があります。
このままだとfloat型だけの対応になってしまい、int型に対応しないんですよね。
例えば囲碁、将棋、チェス、ポケダンシリーズ、神経衰弱、2048とかの碁盤の目のようなゲームだと、駒のおかれる座標は基本的に整数になるのでなるべくintだけで計算させたい...
...floatバージョンとintバージョンを作っていくの面倒ですよね?
同じような宣言を2回もやるのが面倒くさいというそこの貴方に朗報。Templateを使いましょう。
template<typename T> //Template
struct Vector2D {
T x;
T y;
//デフォルトコンストラクタ
Vector2D() noexcept = default;
constexpr Vector2D(const T &x, const T &y) noexcept: x(x), y(y) {};
explicit constexpr Vector2D(const T &v) noexcept :x(v), y(v) {};
}
これでfloatだけでなく、intもOKになりました。
xがintでyがfloatなんていうことは多分ないと思うのでこれでコンストラクタ部分は完成。
5.演算子のオーバーロード
ただ、このままだと各成分ごとに分けて計算する必要があってかなり面倒くさいです。
実際に試してみましょう。試す場合はstructの下へこれをコピペしてください。
#include <iostream>
int main()
{
Vector2D<int> a = { 10,20 };
Vector2D<int> b = { 30,-5 };
Vector2D<int> ans;
#if 0
ans = a + b; //一気に加算はまだできない...
#else
ans.x = a.x + b.x; //xの各要素だけ加算
ans.y = a.y + b.y; //yの各要素だけ加算
#endif
cout << ans.x << "と" << ans.y << endl;
return 0;
}
やってみると、「#if 1」の時はコンパイルエラーが出て「#if 0」の時は正常に動いて正しい計算結果が出力されます。
このままだとVector2D同士を一気に計算させることができず、各要素ごとにちまちまと足し算をする必要がありますね、こんなVector2Dは嫌だ...
ゲームプログラミングなんかをしていると、これらを使って画像,文字の描画や衝突計算をしていくので、このまま使うと当然ソースコードの可読性が下がりますし、グループ開発なんていったらメンバーからウザがられること請け合い。
というわけで、一気に計算させちゃおうっていう魂胆です。
5-1.算術演算子
ここで、算術演算子をVector2D用にオーバーロードします。
四則演算をVevtor2Dでそのままできるようにします。比較・代入演算子は後程。
「+」を例にします。こんな感じで、structの外に記述してください。
struct Vector2D {
//略
}
//加算
template<typename T> inline constexpr const Vector2D<T> operator+ (const Vector2D<T> &v1, const Vector2D<T> &v2) noexcept {
return Vector2D<T>{ v1.x + v2.x,v1.y + v2.y };
}
みたまんまですね。引き算も同じように作ればOKです。
掛け算は
・ベクトル×ベクトル
・スカラー×ベクトル
・ベクトル×スカラー
割り算は
・ベクトル÷ベクトル
・ベクトル÷スカラー
があるのでお忘れなく!
また、割り算はスカラーの引数が0の場合はゼロ除算となるので例外を投げる可能性がありますね、noexceptは消しましょう。
assertなんかしても良いですね~
5-2.比較演算子
続いて比較演算子のオーバーロードです。
とりあえず==を作りましょうか~
struct Vector2D {
//略
}
//==比較
template<typename T> inline bool operator== (const Vector2D<T> &v1, const Vector2D<T> &v2) noexcept {
return (v1.x == v2.x) && (v1.y == v2.y);
}
returnの中身を見ればかなりシンプルなことが分かります。
!=はこれの否定というだけなのでこの結果のnotを返せば良いです。
//return !((v1.x == v2.x) && (v1.y == v2.y)); //これでも良いけど...
return !(v1 == v2); //これで十分!
大なり小なりは要素ごとにやるだけで良いかなと思い作成はしていません。
もし作るのであれば、「こっちより全ての要素が大きければ...」とかですかね?
5-3.代入演算子
代入演算子のオーバーロードもやり方は大体同じです。
ただし、引数の数や定義する場所などが変わってくるので注意しましょう
+=を例に挙げてみます。
struct Vector2D{
//略
//代入加算
inline Vector2D operator+= (const Vector2D &v) noexcept {
x += v.x;
y += v.y;
return *this;
}
}
こちらも乗算の場合はアダマール積やスカラー倍の積が使えますね。
除算はnoexceptを消すことをお忘れなく...
こちらもゼロ除算でassertしても良いでしょう。
5-4.逆ベクトル
struct Vector2D {
//略
}
//逆ベクトル
template<typename T> inline constexpr const Vector2D<T> operator- (const Vector2D<T> &v) noexcept {
return Vector2D<T>(-v.x,-v.y);
}
各要素の符号を反転させるだけですね。
6.汎用性の高い自作関数
ここからは基本的にVector2D内でよく使う関数を自作するという段階になります。
恐らくVector2Dを使っているうちに自分で作りたくなると思うので基礎的なものだけ挙げますね。
6-1.内積・外積
ゲームプログラマーの皆さん、お待たせしました。
ベクトルと言ったら内積・外積、内積・外積と言ったらゲームプログラミングですね?
struct Vector2D{
//略
//内積
constexpr const float Dot(const Vector2D &v) const noexcept {
return (v.x * x) + (v.y * y);
}
}
はい、外積も同じ感じで定義します。外積はCrossで良いでしょう。
return文の中身をちょっといじくればオッケーです。
structの中で定義することで「ベクトル.Dot(ベクトル)」みたいな使い方ができますね。
6-2.ベクトルの長さ
struct Vector2D{
//略
//ベクトルの長さ
const float Length() const {
return std::sqrt(Dot(*this));
}
}
sqrt自体が少し重いらしいですね。どこかの記事で平方根未使用で距離を近似するみたいなやつがあったんですけど...
「まあシンプルにいこうじゃないか」ということで今回はstd::sqrtを採用しました。
なお、std::sqrtはconstexprじゃないのでLengthをconstexprにすることはできません、お気を付けて。
6-3.正規化
struct Vector2D{
//略
inline const Vector2D Normalize() const;
}
//ベクトルの正規化
template<typename T> inline const Vector2D<T> Vector2D<T>::Normalize() const{
return *this / Length();
}
ベクトルの正規化は、キャラクターの斜め移動の速度調整などで多用しますね。
(0,0)→(1,1)への移動速度を1/√2にするとかで意外と汎用性が高いです。
こんな感じで、よく使うであろう関数はガンガン作っていくと良いです。作れば作るほど次第に使いやすくなってきます。
7.実際に使ってみよう!
完成です!
...といいたいところですが、コンパイルが通るのかとか計算が合っているのかとか色々実験してみましょう。
四則演算のみならず、内積・外積とかLengthなどたくさん使ってみると良いです。
計算結果が違っていたら元も子もないですからね、確かめはお忘れなく!
8.完成!
はい、計算結果も無事OKというあなた、お疲れさまでした!
Vector2Dを使っているうちに愛着がわいてくると思います。
なんなら新しい関数をつくりたくなります。良いです、ガンガン作りましょう。
ただ、綻びが出る可能性もあるので定期的にメンテナンスもしっかりしましょう。
次回はこれをDxLibの関数などへ応用していきます。
それでは、良い自作ライフを(^^)/
9.参考にしたサイト