読者です 読者をやめる 読者になる 読者になる

ゲーム開発日記 2016年11月前半

ゲーム開発

これ以上ブログ放置するとサボり癖が出そうなので、箇条書きで進捗メモ。

f:id:giraffyk1:20161122110328p:plain

  • GUIまわりをドット絵で暫定的に作成。アイテムアイコンなんかもいくつか書いてみる。楽しい
  • メニューとGUI・キャラチップなどいくつか表示できそうなものが出来たのでプログラム再開
  • オープニングメニューなんかをスプライトシート化したり、プレイヤーの設定ファイルの仕組みを整えたり
  • 土台部分でC++で上手く書けない箇所があったので結構リファクタリング(型とかテンプレートの問題です。アップキャストはごくごく例外を除いて方針)
  • Lua実行時にエラーがあってもC++11環境下ではLuabindがエラーをハンドリング出来ないので、クラス登録以外の部分のコネクションを自分でLua API経由で書き直し
  • ゲーム起動〜メニュー部分のコードをもりもり書いているところ

普通はゲーム部分のプロトタイプから作って面白さを確認しながらスクラップビルドするのがゲーム開発の王道だと思います。でも僕の場合はゲームメニューやらオプションやらを先に実装しています。理由はC++に慣れていないので、コードを書けば書くほど「これはC++で上手く書けるんだろうか」という不安が付いてまわるためです。簡単な部分から書いてC++の練習をしつつ、プログラムの基礎部分にヤバいものが見つかったら大胆にリファクタリングしていく。という事を繰り返しています。

おかげで全然開発が前に進んでいません。

が、ようやく土台部分もゲームを完成させるに足る程度にはしっかりした気がしてきました。技術的な不安も多くが解消されつつあるので、ここからは少しずつペースが上がる予定です。

開発期間がすでにC++の基礎学習部分をのぞいて1ヶ月半になりそうなのに、ゲーム部分がまったく開発できていないのに自分自身驚愕しているところです。やっぱり初心者がC++でフルスクラッチに近い状態でシミュレーションゲームのようなものを作るのはとっても大変なようです。あたりまえですが。

モチベーションが維持できているかというと、趣味なのでできているようないないような。最近は少しC++を書くのがダルいなあと感じ始めています。絵書いたりLuaでスプライト動かしたりするのはとっても楽しいんですけどね。


年が開ける頃にはキャラクターがフィールドを歩けているといいなあ。

ゲーム開発日記 2016年10月後半2

ゲーム開発

 というわけでタイトル画面を仮組みしてみた。
f:id:giraffyk1:20161101204450p:plain

 ゲームタイトルはVILLAGISM(ビレッジズム)という名称に仮決め。ロゴデザインも仮で、ある程度完成形の8ビット風のイメージが出来るようなところまで。

 背景画像は壁紙サイトさんから一時的にお借りしたもので、イメージに近いものを仮置き。このまま使えたらなんて思ったんだけど、件の壁紙サイトさんでは権利関係に一切言及が無かったので、残念ながらいつか置き換わる予定。これくらい描けたらなあ。

 Photoshopはブロク写真の編集なんかでAdobe Creativeなんとかを年払いしていたのでこれを利用。Lightroomとあわせて月額980円だかのサービスなので、Illustratorは使えない。使えても使えこなせないからいいんだけども。それを言えばPhotoshopも全然使えない。無い無い言い出すとゲームのインターフェイスデザインも勿論初体験なので、キリがない。

 グラフィックは未知の領域だらけだし、イラストとなるとまったく描いたことが無い。だから8ビット風を今回のプロジェクトに採用しました。どうにか誤魔化そうという魂胆です。ドット絵ならばプロと差が出にくい、プレイヤーが勝手に良い方へ補正して見てくれるんじゃないかという考えです。甘っちょろい考えですが、一人でちまちま作りたいのだからしょうがない。この目論見がある程度達成出来なければここでこのプロジェクトもお蔵入り。


 というわけで、ドット絵描いてみますか! と10日くらいずっとドット絵描いていました。

www.aseprite.org

ドット絵エディターを軽く調べて、OSXで動作するもので評判が良いものとしてAsepriteが挙がっていたのでこれを採用する。
$14.99ナリ。書籍代以外で初の出費です。

f:id:giraffyk1:20161101210944p:plain

 他のエディターを使った事が無いので比較できないのですが、すごく使いやすい。これくらいは描けるようになってきました。

 イラストは苦手分野、というかまったく未知の分野だったので、ちょっと援護射撃が必要だろうということでAsepriteを買って正解でした。それと、安いペンタブも援護射撃として購入。

 まだまだ全然真っ直ぐ線も引けないけども、横幅20ドットくらいなので書いたり消したり時間を描けたらある程度見えのする形が浮かび上がってきます。

 キャラクターについては、僕はレゴが好きなのであれに近いプロポーションや色数をまずイメージしてました。それから過去のゲームのドット絵を参考に。任天堂のドット絵はどれも魅力的で大いに参考にしています。ポケモン、ゼルダの伝説、Motherあたりに強く影響を受けています。ポケモンは緑を、ゼルダは64まで、Motherは1−3まで全部やったことないですけど…。

 ともかくAsepriteに慣れつつ、ドット絵になれつつ、絵を描くこと自体になれつつ、んでもってアニメーションにも慣れる。

こういう素体をずーっと描いていました。絵を描くのは案外楽しかったです。発見。

で、いくつかのキャラと地面とを描いて1枚にしてみる。

f:id:giraffyk1:20161101213010p:plain

 自画自賛とはいかないものの、なんとかゲームとして成立しそうな雰囲気が出ていてホッとする。
水色の部分はインターフェイスが置かれる予定の場所。

 いくつかの静的なオブジェクトと、キャラ(動的なオブジェクト)、それからインターフェイスのデザインが終わったらまたプログラムのほうに戻る予定です。

ゲーム開発日記 2016年10月後半1

ゲーム開発

 2週間弱空きましたが、引き続きゲームを制作中です。

 前回からプログラム的に進んだところは、Luaをゲームシステムに組み込んでゲーム内に登場するオブジェクトをLuaで操作できるようにしました。どこまでLuaで、どこまでC++でという領分の切り分けをずっと悩んでいたのですが、オブジェクトの作成と削除以外はLua側で、というあたりを落とし所にしようかな、と思っています。オブジェクトやリソースをLuaに操作させるとオブジェクトのpoolingだったりリソースの再利用をLuaでじゃあするのかという話になってくるので、ここを線引きとしました。とはいえLuaからC++のメソッドを叩いて子オブジェクト作成リクエストをしたり、なんてのはできるようにする予定。

 それからシーンやレイヤーの管理方法を全体的にスッキリさせました。静的型付け言語はこういったリファクタリングを気軽に出来るのは大きなメリットだと思う。動的型付け言語だとテストが無いとちょっとやりたくない。

 とここまできて、次はスプラッシュやタイトルシーンの仮組み。このあたりからゲームに組み込む絵があったほうが作業が進めやすいので、ビジュアル関連の作業に手を出す事にしました。プログラミングも疲れてきたのでちょうど良い頃合いかなって。C++にはだいぶ慣れてきたけど、Luaの組み込みがちょっとダルかった。

 というわけで、ここからいきなりグラフィックの話になるので、日記を分ける事にします。

C++とLuabindでインスタンスのやり取り

C++ ゲーム開発

現在の個人プロジェクトでLua組み込んでみよかーと思ったので備忘と誰かのために。

Lua Luabind on OSX

Luabind公式のソースでOSXではソースからコンパイルできなかった。Makefileをちょろっと見たけど拡張子の指定とかがOSX用で無かったり、ネットで見つかるOSXの指定オプションが無視されたりとよくわからない状態だったのでbrewからインストールする。

~$ brew install luabind

依存のboostも合わせてインストールされる。

ソースとコンパイル

メインプログラムのソースとヘッダはこんな感じ

#include <iostream>
#include <luabind/luabind.hpp>
// Luaライブラリ
extern "C" {
    #include "lualib.h"
}

これは他所サイトでもよく見かけるので解説はそれらを参考にしてください。で、コンパイル。

私の場合は動的ライブラリの位置とファイル名が
/usr/local/lib/libluabind.dylib
/usr/local/lib/liblua.5.1.5.dylib

だったので、

clang++ .main.cpp -g -O0 -std=c++11 -stdlib=libc++ -o ./a.out -llua.5.1.5 -lluabind

とライブラリ名を指定してやる。lib○○.dylibの○○部分にハイフンを付ける。dylibじゃなくファイル名がsoでも多分おなじ。
brewからインストールした場合、/usr/local/libに上記のようにlua5.1とluabindの動的ライブラリが無い場合があるかもしれない。その場合は/usr/local/Cellar以下や/usr/local/opt以下にないか確認して/usr/local/lib以下にリンクを貼ると良い。多分貼らなくても自動で見つけてくれる気がする。

C++とLuaの連携について

スクリプト言語による効率的ゲーム開発 新訂版 (LuaとC/C++連携プログラミング)

スクリプト言語による効率的ゲーム開発 新訂版 (LuaとC/C++連携プログラミング)

入門用に上記の書籍に目を通した。LuabindではなくLuaAPIを生で触る所から解説されてある。わかりやすさも心がけて書かれているのでとても読みやすい。ネットに日本語の情報が少ないので最初は書籍に頼ったほうがいいかもしれない。(私は図書館で借りてきたこの1冊だけ)

で私はイロイロだるかったのでluabindというちょっと高級なライブラリから連携を取ることにする。c++側のクラス登録も楽勝。この辺は他サイトに譲る。

luaでc++側のクラスを扱うにはluabindで登録していくだけで基本OKなんだけども1つ注意点として、Luaの知らない戻り値型をluaで受け取らせるとエラーで落ちてしまう。なので自作の巨大なクラスをLuaで自由自在に扱わせようとすると全ての型をluabindで登録しなくちゃいけない。

上記書籍では話を簡単にするために、登場するサンプルコードが極力lua側で物事を解決しようとしているので、あまり上記のような問題は検討されていない。でも一般的には、C++とLuaでインスタンスが行ったり来たりするのが現場の普通だと思われるので、いっちょテストコードを書いた。
main.cpp:

#include <iostream>
#include <luabind/luabind.hpp>
// Luaライブラリ
extern "C" {
    #include "lualib.h"
}

// Luaでインスタンスを取り扱うためのテストクラス
class TestClass {
public:
    // コンストラクタとデストラクタ
    TestClass() {}
    ~TestClass() {}
    // 文字列を返すメンバ関数
    std::string hello() {
        return "hello world";
    }
    // 数値を保持するメンバ変数
    int my_count = 0;
};

int main() {
    // Luaハンドラ(VM)とファイルオープン
    lua_State* L = lua_open();
    if (luaL_dofile(L, "./luafile.lua")) {
        // エラーは強制終了
        std::cerr << "ERROR : " << lua_tostring(L, -1) << std::endl;
        lua_close(L);
        abort();
    }
    // Luaライブラリ利用
    luaL_openlibs(L);
    // Luabindとヒモ付け
    luabind::open(L); 
    luabind::module(L) [
        // クラスの登録
        luabind::class_<TestClass>("TestClass")
            // コンストラクタ登録
            .def(luabind::constructor<>())
            // helloメンバ関数登録
            .def( "hello",&TestClass::hello)
            // countメンバ変数登録
            .def_readwrite("my_count", &TestClass::my_count)
    ];
            // Basic sf types
    // TestClassインスタンス生成
    TestClass test_instance;

    // Lua側のlua_main関数に引数でTestClassインスタンスを渡しつつ呼び出し
    luabind::call_function<void>(L, "lua_main", &test_instance); // 参照渡しにしないとコピーされるので注意
    // C++側でも値の保持を確認してみる
    std::cout << "welcome back C++" << std::endl;
    std::cout << test_instance.my_count << std::endl;

    // Lua側でTestClassインスタンスを作成してみる(戻り値はTestClassの参照)
    TestClass &test_made_lua = luabind::call_function<TestClass&>(L, "make_instance");
    std::cout << "welcome back C++ again" << std::endl;
    std::cout << test_made_lua.my_count << std::endl;

    // 用が済んだらLuaクローズ
    lua_close(L);
    return 0;
}

luafile.lua:

function lua_main(test_instance)
	print("hello from lua");
	-- インスタンスtest_instanceからTestClassメンバを操作してみる
	-- メンバ関数はインスタンス名:メンバ の構文でアクセス
	print(test_instance:hello());
	-- メンバ変数はインスタンス名.メンバ の構文でアクセス
	print(test_instance.my_count);
	test_instance.my_count = test_instance.my_count + 1;
	print(test_instance.my_count);
	test_instance.my_count = test_instance.my_count + 1;
	print(test_instance.my_count);
	test_instance.my_count = test_instance.my_count + 1;
	print(test_instance.my_count);
end

function make_instance()
	-- TestClassインスタンスをLua側で生成
	test = TestClass();
	-- 値を保持させて返してみる
	test.my_count = 999;
	return test;
end

Result:

hello from lua
hello world
0
1
2
3
welcome back C++
3
welcome back C++ again
999

ポインタは普通に渡せる

上記のコードのポイントは抜粋したcall_functionを使った2行

    luabind::call_function<void>(L, "lua_main", &test_instance);

    TestClass &test_made_lua = luabind::call_function<TestClass&>(L, "make_instance");

call_functionの引数は第一にVMのハンドラ、第二がlua側の呼び出したい関数名、第三以降にlua側の関数が受け取る引数を指定する構文になっているのですが、ここにポインタが普通に渡せます。参照で渡さないとコピーされるので注意。取り扱えるのはlualib.hで定義されている基本的な型と上記コードのluabind::class_のように教えてあげた型だけかと思われる。

2行目は戻り値として同じくTestClass型のポインタを受け取っている。

1行目があれば、c++で生成したインスタンスをlua側でゴニョゴニョさせれる。lua側のグローバル変数だかクロージャに突っ込んでおけば、C++側のentityとluaのVMを対で連携させていろいろ処理ができる。

2行目は、luaで作ったインスタンスをC++で受け取っているので、処理の要なんかをC++で受け持ちたい場合もこれで対応可能。

test_instance:hello();
test_instance.my_count;

Lua側では:コロンと.演算子でインスタンスとメンバをつなぐ。Luaで作ったインスタンスと同じ構文でC++から持ち込んだインスタンスが扱えるようです。

Luabindのヘッダはとても重いのでプリコンパイル化必須

#include <luabind/luabind.hpp>

この一文がめちゃ重い。中は魔宮のようになっていてコンパイルするとテンプレートがばんばん展開されるらしい。Luabindとトレードオフだとしても許容できないくらい重い。これ1つでコンパイルが10秒以上は伸びる。

cpplover.blogspot.jp

ここを参考にpchにすることで、1/10くらいの速度になりました。一安心。

2016年10月中盤のゲーム開発日記

ゲーム開発 雑記

10日ほど日記が開いてしまいましたが、ゲームは鋭意開発中です。骨組み部分なので見た目には何も進んでいません。開発を初めて約3週間ですがやってきた事はというと…。

  • ゲームエンジンの骨子部分の開発
    • 全ての要素(entity)の元になるベースクラス
    • 親子entityの直接会話(実際には親から子へのみ)
    • スプライト管理、アニメーション管理機能(Luaに移行予定)
    • 状態によってクラスや関数ポインタを切り換えて挙動が変わるシステム
    • アプリケーショングラフ(アプリケーション→シーン→レイヤー→オブジェクト)
  • ゲーム遷移
  • オブジェクト間のメッセージングシステム(遅延対応)
  • キーイベント
    • 設定によるキーバインディング対応
    • シーン毎のキーイベント処理システム(コールバック対応)
    • window/フルスクリーンモード対応(OSXの仕様でちょい微妙)
  • iniファイルみたいな、外部ファイルによるConfig機能(TOMLで)
  • luaとluabind動作テスト
  • 設定シーン用にTgui動作テスト

と大仰に書けばいろいろやっています。さらにC++自体の学習も並行していたので、コードも行きつ戻りつしています。プロトタイプではクラス設計を適当にすればよかったのですが、本組みとなるとそうもいきません。土台を固めようとちゃんと書いていると、アップキャスト、ダウンキャスト、vtableとvirtualメソッド、テンプレートなどの仕組みをキッチリ理解させられるハメになりました。

動的型付けのスクリプトの世界から来た僕からすると、この強烈な静的型付けの世界では、便利な事はだいたい速度とはトレードオフな世界で、どうクラス設計をすればいいか本当に悩みました。せっかくなのでできるだけC++っぽくなるように設計しています。そうしていくと静的型付けの構文チェックやデバッグ機能が最強に機能して、そういう部分では開発が楽になってきました。こういったものを乗り越えて、3週間ほどでだいぶC++コンパイラとの会話のキャッチボールにも慣れてきて、コードを通すという部分で苦労する事は無くなってきました。

ここからは…あと1〜2週間引き続き骨組み部分の開発だと思います。

  • Luaの本格的な組み込み
  • Tguiの本格的な組み込み
  • スプラッシュ・タイトル・設定シーンの仮組み
  • コマンドラインツールの組み込み(ゲーム走らせたままLuaのリロードとかシーンジャンプとかに使う予定)
  • ゲーム部分の取り掛かり
    • といってもデバッグ機能の実装から

あたりが出来たらなあと思っています。ここまで来たら、絵とプログラミングが半々くらいになって「ゲーム作ってるぞ!」といった風情が画面から出てくるかなあ。

c++でスマートポインタを使いつつ複雑なデータ構造を実現する(キーバインディングのために)

ゲーム開発 C++

昨日くらいからゲームで使うイベントハンドラを作っています。そこで各イベントに割り当てられたコールバック関数を管理するデータ構造を考えていています。hashの添字でアクションをバインドして、そこからコールバックを呼ぶみたいなやつ。これを管理するわかりやすいデータ構造とコールバックの関係は、

アクションハッシュ[アクション名1]-*コールバック関数ポインタ
アクションハッシュ[アクション名2]-*コールバック関数ポインタ
アクションハッシュ[アクション名3]-*コールバック関数ポインタ
アクションハッシュ[アクション名4]-*コールバック関数ポインタ

みたいなのが分かりやすい。

アクションハッシュ[ジャンプボタン]-*ジャンプを実現させる関数ポインタ
アクションハッシュ[パンチボタン]-*パンチを実現させる関数ポインタ
アクションハッシュ[キックボタン]-*キックを実現させる関数ポインタ
アクションハッシュ[必殺技入力]-*必殺技を実現させる関数ポインタ

と書くともうちょっとわかりやすいでしょうか。

しかし、このままだと1アクションに1機能しか割り当てができない。また、シーンの移動などで、ジャンプが出来ない状態になった時、コールバックを削除する際に関数ポインタの型みたいなのを総なめにしないといけない。

これらを解決するために

アクションハッシュ[アクション名]
 ┗アクションに対応したコールバック配列
   ┣コールバック情報が入った構造体1
   ┣コールバック情報が入った構造体2
   ┗コールバック情報が入った構造体3

みたいなデータ構造を検討してみた。アクションへはo(1)で到達できるし、コールバックのリストは基本1つか多くても片手で数えられるはずなので、そこを線形処理しても大してコストはかからないだろうというわけ。最終情報が構造体なのは、IDで管理したりコールバック属性(他のリスナーを排他にするかとか、スリープとか)を管理するため。

でテストで書いたコードがこれ

#include <iostream>
#include <deque>
#include <unordered_map>

// 上の例でいえばこのクラスが最終的なデータ=コールバックが入った構造体と同じ場所に位置する
class TestClass {
public:
    int my_num = 0;
    TestClass(int i) : my_num(i) {}
    void hello(){
        std::cout << "hello world:" << my_num << std::endl;
    }
}; 
 
int main() {
    // 最終データをスマポでくるんだ宣言
    using sPtrTestClass  = std::shared_ptr<TestClass>;
    // さらにそれをスマポでくるんだdeque(上記の例でいえば配列)
    using sPtrDequeTest  = std::shared_ptr<std::deque<sPtrTestClass>>;
    // さらにそれをスマポでくるんだunordered_map。キーは文字列型
    using sPtrTContainer = std::shared_ptr<std::unordered_map<std::string, sPtrDequeTest>>;

    // 上記までがデータ構造の準備。以下データの登録

    // 最終データを3つくってみる
    sPtrTestClass t1{ new TestClass(1)};
    sPtrTestClass t2{ new TestClass(2)};
    sPtrTestClass t3{ new TestClass(3)};

    // 最終データをdequeに3つ放り込む
    sPtrDequeTest ary{new std::deque<sPtrTestClass>{t1, t2, t3}};

    //  アクションを登録するハッシュの初期化
    sPtrTContainer map{new std::unordered_map<std::string, sPtrDequeTest> {}};

    // テストという添字でdequeを突っ込む
    (*map.get())["test"] = std::move(ary);

    // アクション"test"に登録されているデータの呼び出し例。実際はイテレータで先頭からコールバックを呼び出すことになる
    (*(*map.get())["test"].get())[0]->hello();
    (*(*map.get())["test"].get())[1]->hello();
    (*(*map.get())["test"].get())[2]->hello();

}
出力:
hello world:1
hello world:2
hello world:3

usingほげほげの部分がまず何をしているか意味がわかりづらい。これはデータ構造が3段階で、そのままデータ構造を表現するとあほみたいな表記になるので、3段階それぞれにusingで別名を付けています。それをしなかった場合、アクションを登録するハッシュの型は

std::shared_ptr<std::unordered_map<std::string,  std::shared_ptr<std::deque<std::shared_ptr<TestClass>>>>>;

ということになる。3層構造ってだけでもう取り返しが付かないシンタックス。

また呼び出し部分

(*(*map.get())["test"].get())[0]->hello();

これもごちゃごちゃしてわからない。これ何してるかというとstdのスマートポインタには添字アクセスのオペランドを処理する機能が付いていない。そのため、生ポインタを取得する.get()を2度(ハッシュの添字解決、dequeの添字解決)行っている。->みたいにアクセスできるもんだとてっきり思ってたが、そうではないらしい。

map->["test"]->[0]->hello();

みたいなのをイメージしてくれたら良い。

できないことはないC++なので、上記の表記もきっと実現できるんだろうけど、ここまで書いていったい僕は何をやっているんだろうという気持ちになってきて、やめました。

とりあえず目的のデータ構造は実現できたが果たしてコレが必要なのだろうか。

本当に必要なの?

気になった所としては

  • 適当にstd::vectorやarrayに格納してforで回したほうが速いんじゃないか?
  • 各オブジェクトの生成コストのが高いんじゃないか?
  • こんなゴミみたいなコードほんとにみんな書いてるのか?

コードを高速化する手段 – 二流プログラマの三流な日常

上記ページなんかではvectorをmapやlistに代替えするにはそれ相応のデータ規模でないとむしろ遅くなるという言及があります。やはり、100や200のキーバインドにこんなデータ構造を使うのはバカげている気がしてきます。
baptiste-wicht.com

std::vector and std::deque perform always faster than std::list with very small data
(小さなデータを扱う時、std::listよりstd::vectorやstd::dequeのほうが常に速い)

ふうむ。

僕はWeb出身です。なので、データへのアクセスに関しては線形探索は絶対避けるべき。ソートもまず避ける。できるだけo(1)を目指すのが正義だと思っています。だからこそ今回のようなデータ構造を実現しようとしてきたわけです。しかし、この発想の根源というのは、秒間数万だとか数百万リクエストに対して数万数十万のユーザーを識別しつつスケーラビリティの乏しいDBを保護するためのものなわけです。これがスタンドアロンな実行環境になると、1度頭を切り替えたほうが良いと思えてきました。ざっくり言えばループで総当りしたほうが良い場合のほうが多いのではないか?

アクションへのo(1)アクセスのために添字が使いたいからといって、バインディングマップを馬鹿正直にunordered_mapで管理する必要があるのかもちょっとした疑問が出て来ます。添字はenumでいいのでは? じゃあvectorで良いということになります。

また、プレイヤーから入力される情報は限られています。キーボードのキーの数、マウスのカーソル位置とボタンが3つくらい、あとはゲームパッド。これらの中からゲームが対応すべき入力の種類なんて、いいとこ10や20なんじゃないか。じゃあ、mapじゃなくてゲームに予想されるアクションを変数で確保しておくこともやぶさかじゃないんじゃないかなあという考えにも行き着く。

さらに1つのアクションに対するコールバックを複数管理する必要性もここまでくると良くわからなくなってきた。たとえばジャンプボタンを押すと2人のキャラが同時にジャンプするにしたって…。1つのコールバック関数で管理してコールバック関数側で2人分を動かしたほうがいいんじゃないか。


などと考えているため全然ゲーム制作が進みません。。

※最終的にステート単位でキーバインドを管理する事にしました

make Makefileで階層(サブディレクトリ)を走破しながら依存を解決してコンパイルする

C++

C++でクラス毎にファイルを分け、ファイル群をディレクトリ構造で管理するのは一般的な方法かと思います。ただ階層化したディレクトリの依存関係を解決しながら1つの実行ファイルを生成するのにはコツがいるようです。

一般的には、サブディレクトリ毎にMakefileを設置して多段コンパイルするようですが、1つの目的で複数のファイルがあるとメンテナンス性も落ちるしトラブルを孕みやすいので、1つのMakefileで解決する方法がないか模索しました。

というわけで以下のMakefileで目的が達成されました。

# コンパイラ 僕はOSXでC++なのでclang++
COMPILER  = clang++
# お好きなフラグを
CXXFLAGS    = -g -O0 -MMD -Wall -Wextra -Winit-self -std=c++11 -stdlib=libc++
# ライブラリ関係の指定
ifeq "$(shell getconf LONG_BIT)" "64"
  LDFLAGS = -L/usr/local/lib
else
  LDFLAGS = -L/usr/local/lib
endif
LIBS      =
# インクルードパスの指定。これをちゃんとしておかないとDEPENDS(依存)ファイルがうまく作れない
INCLUDE   = -I../include -I/usr/local/include
# 生成される実行ファイル
TARGETS   = a.out
# 生成されるバイナリファイルの出力ディレクトリ
TARGETDIR = ../bin
# ソースコードの位置
SRCROOT   = .
# 中間バイナリファイルの出力ディレクトリ
OBJROOT   = ../obj
# ソースディレクトリの中を(shellの)findコマンドで走破してサブディレクトリまでリスト化する
SRCDIRS  := $(shell find $(SRCROOT) -type d)
# ソースディレクトリを元にforeach命令で全cppファイルをリスト化する
SOURCES   = $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.cpp))
# 上記のcppファイルのリストを元にオブジェクトファイル名を決定
OBJECTS   = $(addprefix $(OBJROOT)/, $(SOURCES:.cpp=.o)) 
# ソースディレクトリと同じ構造で中間バイナリファイルの出力ディレクトリをリスト化
OBJDIRS   = $(addprefix $(OBJROOT)/, $(SRCDIRS)) 
# 依存ファイル.dを.oファイルから作る
DEPENDS   = $(OBJECTS:.o=.d)

# 依存ファイルを元に実行ファイルを作る
$(TARGETS): $(OBJECTS) $(LIBS)
	$(COMPILER) -o $(TARGETDIR)/$@ $^ $(LDFLAGS)

# 中間バイナリのディレクトリを掘りながら.cppを中間ファイル.oに
$(OBJROOT)/%.o: $(SRCROOT)/%.cpp
	@if [ ! -e `dirname $@` ]; then mkdir -p `dirname $@`; fi
	$(COMPILER) $(CXXFLAGS) $(INCLUDE) -o $@ -c $<

# 依存関係ファイル
-include $(DEPENDS)

ディレクトリ構成は以下を想定しています。

example
|-- bin
|   `-- a.out   <- 出力される実行ファイル
|-- include
|   |--sub
|   |   `-- sub_example.h   <- 階層化されたクラスのヘッダファイル
|   `-- example.h
|-- obj 
|   |--sub    <- src以下のディレクトリ構成を反映させたディレクトリ(自動生成)
|   |   |--  sub_example.d    <- 階層化されたクラスの依存関係ファイル(自動生成)
|   |   `-- sub_example.o    <- 階層化されたクラスの中間バイナリファイル(自動生成)
|   |-- example.d
|   `-- example.o
`-- src
    |--sub
    |   `-- sub_example.cpp   <- 階層化されたソース・ファイル
    |--  Makefile     <- example/srcが起点
    `-- example.cpp

ベースは以下を参考にさせてもらいました。CXXFLAGSの解説は以下を参考にしてください。
urin.github.io

また問題が出たら修正版を書こうと思います。

最近はCMakeやSConsというモダンなタスクランナーが存在するのでMakeに頼る必要はもはやないかと思いますが、Makeでどうしても、という人はご参考にどうぞ。