色々あって、チーム内で簡単なゲームを作りあって競おう、という話になりました。
プラットフォームやツールなどは自由なのですが、元々C++に興味があったのと、Cocos2D-xはやってみたかったので、思い切って挑戦することにしました。
とはいえ、そもそもC++を書いたことがほぼ皆無なので、色々調べるところからスタートです。
なので、C++というよりは、Cocos2D-xを触っていく仮定で出てきた疑問やメモなどがメインになります。
ちなみに今回作ったゲームは一番下にリンクがあるので、ぜひ遊んでみてください(๑'⌓'๑)
柔軟な配列を利用する
C言語などでは通常、配列は固定長です。
しかし、最近の言語では柔軟に配列長を変えることが可能になっています。
C++ではstd::vector
やstd::list
、std::map
などの コンテナ と呼ばれる機能を使って実現するようです。
具体的な使い方は以下。
// vectorというヘッダファイルを読み込む。
#include <vector>
// std::vector<T>で型を指定
std::vector<Hoge::Fuga*> fugas;
Hoge::Fuga *fuga = new Hoge::Fuga;
// push_backで配列の末尾に追加
fugas.push_back(fuga);
格納した要素をイテレータで回す
配列に格納したものは大抵、すべての要素にアクセスしてなにかをしたいはずです。
その場合は イテレータ を使って以下のようにします。
for (std::vector<Hoge::Fuga*>::iterator itl = fugas.begin(); itl != fugas.end(); ++itl) {
// do something.
}
ちなみにイテレータの型は長くなるのでauto
を使うと楽。
for (auto itl = fugas.begin(); itl != fugas.end(); ++itl) {
// do something.
}
さらに簡潔な書き方
for (Hoge::Fuga *itl : fugas) {
// do something.
}
//////////////////////////////////////////////
// autoを使ってさらに簡潔に
for (auto itl : fugas) {
// do something.
}
実際にコンパイルが通るコード例
#include <vector>
namespace Hoge
{
class Fuga
{
public:
void method();
};
void Fuga::method() {
printf("fuga");
};
};
int main() {
Hoge::Fuga *fuga1 = new Hoge::Fuga;
Hoge::Fuga *fuga2 = new Hoge::Fuga;
Hoge::Fuga *fuga3 = new Hoge::Fuga;
std::vector<Hoge::Fuga*> fugas;
fugas.push_back(fuga1);
fugas.push_back(fuga2);
fugas.push_back(fuga3);
for (auto itl = fugas.begin(); itl != fugas.end(); ++itl) {
// イテレータ変数は配列内のポインタなので(*hoge)で実体にアクセス
(*itl)->method();
}
return 0;
}
イテレーション中に要素を削除する
上記のようにイテレーションを回しながら、特定の条件で要素を消したい、という要望があると思います。
ただ、単純に削除を実行してしまうとイテレーションが指すポインタがおかしくなるため、クラッシュしてしまいます。
そこで、以下のようにすることで安全に削除を実行できます。
(ちなみにこちらの記事を参考にさせていただきました)
// ++itlとしていた箇所を削除
for (auto itl = fugas.begin(); itl != fugas.end();) {
if (/* 削除する判定処理 */) {
// `erase`メソッドの戻り値を新しいポインタに指定
itl = fugas.erase(itl);
continue;
}
// for文内でイテレーションを進める
itl++;
}
std::removeを使って該当要素を削除する
色々調べていくうちに、さらに簡単に該当の要素を削除する方法がありました。
std::remove
を使って削除する方法です。
最初、どういう動作をしているのか分かりづらかったですが、分かってしまえばとても便利なものでした。
std::vector<int> list;
list.push_back(1);
list.push_back(2);
list.push_back(1);
list.push_back(3);
list.push_back(1);
list.push_back(4);
list.push_back(1);
list.push_back(5);
// std::removeを使って要素を切り詰める
std::vector<int>::iterator it = std::remove(list.begin(), list.end(), 1);
list.erase(it, list.end());
以上のようにすると、std::remove
を実行した時点で1
の要素が削除されます。
されますが、「要素数」はそのままで削除されずに残った要素が切り詰められます。
どういうことかというと、
最初の状態
[0] | [1] | [2] | [3] | [4] | [5] | [6] | [7] |
---|---|---|---|---|---|---|---|
1 | 2 | 1 | 3 | 1 | 4 | 1 | 5 |
std::remove
を実行した状態
[0] | [1] | [2] | [3] | [4] | [5] | [6] | [7] |
---|---|---|---|---|---|---|---|
2 | 3 | 4 | 5 | 1 | 4 | 1 | 5 |
std::remove
により1
の部分が削除されます。
削除後は「2, 3, 4, 5」の要素だけが残ります。
この4つの要素が先頭から切り詰められます。
ただ、最初に書いたように「要素数は変化しません」。
どういうことかというと、先頭から4つの要素は切り詰められて「2, 3, 4, 5」が入りますが、残った要素は「そのまま」です。
なので最初の配列と見比べてみると後ろ4つの数字が同じことが分かります。
さて、消したあとは要素数も切り詰めたいですね。
それが最後の行で実行していることになります。
list.erase(it, list.end());
実はstd::remove
を実行した時点で、it
は切り詰めたあとの位置をポイントしています。
(^
の位置にポインタがある)
[0] | [1] | [2] | [3] | [4] | [5] | [6] | [7] |
---|---|---|---|---|---|---|---|
2 | 3 | 4 | 5 | ^1 | 4 | 1 | 5 |
そしてlist.erase(it, list.end());
はit
が示した位置から要素の最後までを削除、という指定になります。
こうすることで無事、該当要素を削除したのちに要素数も削除後の状態と一致するようになりました。
特定の要素だけ削除する
std::vector
から要素を削除するには前述の通りerase
メソッドを使います。
しかし、erase
メソッドの引数はイテレータの位置を渡す必要があります。
そのため、特定の要素を削除したい場合はまずそれが格納されている位置を取得してから削除を実行します。
コードは以下。
// hoge変数に特定のインスタンスが入っているとする
std::vector<Hoge*>::iterator pos = std::find(hogeList.begin(), hogeList.end(), hoge);
// 要素が見つかったかどうか確認する
if (pos != hogeList.end()) {
hogeList.erase(pos);
}
ラムダ式
クロージャや関数オブジェクトなど、言語によって呼び方は様々ありますが、そうした関数に相当するものを渡すことができる機能です。
(ちなみにコマンドラインでコンパイルするにはg++ -std=c++11 -o lambda1 main.cpp
やg++ -std=c++0x -o lambda1 main.cpp
のようにオプションを指定する必要があるようです)
シンプルな書式は以下の通り。
[](int a, int b) -> int { return a + b; }(1, 2);
見ればなんとなく雰囲気が伝わると思います。
それぞれの意味は以下の通り。
- [] ... ラムダキャプチャ
- () ... パラメータ定義節
- {} ... 関数ブロック
- () ... 関数呼び出し式
キャプチャ
ラムダ式の外で定義した変数を、ラムダ式の中で使用したい場合は「キャプチャ」をする必要があります。
キャプチャには「参照キャプチャ」と「コピーキャプチャ」の2種類があります。
参照キャプチャ
参照キャプチャは以下のようにします。
int externalInt = 0;
[&]() {
externalInt += 1;
return 0;
}();
[]
の中にキャプチャの種類を書きます。
&
の場合は参照キャプチャとなり、変数にアクセスすることができます。
参照渡しなどの場合にも&
を使うので分かりやすいですね。
コピーキャプチャ
参照キャプチャの対としてコピーキャプチャがあります。
int copiedInt = 5;
[=]() {
int a = 3 + copiedInt;
// 以下のように代入しようとするとエラー
// copiedInt = 3;
}();
コピーキャプチャは値をコピーとしてキャプチャします。
そのため、内部で上書きすることができません。
mutable指定のコピーキャプチャ
以下のようにすることで、代入も可能になります。
なりますが、参照ではないため、ラムダ式の中で変更しても、外の変数には影響がありません。
int mutableInt = 3;
[=]() mutable {
int a = mutableInt + 3;
mutableInt = 5; // 再代入できる
}();
ラムダ式はstd::function型の変数で保持できる
ラムダ式はstd::function<T>
型の変数で保持できます。
// 定義
std::function<int(int, int)> f() {
return [=](int a, int b) -> int { cout << (a + b) << endl; return 0; };
}
// 利用例
auto func = f();
func(1, 2); // => 3
若干ややこしいことをしていますが、f()
関数がラムダ式を返す関数になっています。
つまり、std::function<int(int, int)>
が戻り値の型です。
<int(int, int)>
の部分がラムダ式自体の引数と戻り値の型ですね。
継承修飾子
C++でのクラス継承は以下のように書きます。
class Hoge : public Fuga {
//
}
このときのpublic
が「継承修飾子」と呼ばれるもの。
ここに指定できるのはいわゆるpublic
、protected
、private
の3つ。
それぞれ以下の意味があります。
継承修飾子 | 意味 |
---|---|
public | 基底クラスで設定したアクセス修飾子をそのままで継承する |
protected | 基底クラスでpublic だったものをprotected に引き上げて継承する |
private | 基底クラスの全メンバをprivate として継承する |
クラス
C++はオブジェクト指向言語なのでクラスという概念が言語レベルで実装されています。
クラスを定義するには以下のように宣言します。
class Hoge
{
public:
int age;
string name;
}
// Hogeを継承
class Fuga : public Hoge
{
// additional for fuga class
}
クラスの宣言にはclass
を使います。
また継承する場合は、クラス : [public|protected|private] 継承元クラス
として継承します。
コンストラクタ
最初、C++のコンストラクタを見た時に若干「???」となりましたが、分かってしまえば単純です。
通常のコンストラクタ
class Hoge
{
public:
Hoge() {}; // なにもしないコンストラクタ
}
// --------------------
// 宣言と実装を分ける
class Hoge
{
public:
Hoge();
}
Hoge::Hoge() {}; // なにもしないコンストラクタ
初期化を伴うコンストラクタ
クラスには生成時に初期化しておきたいメンバ変数がある場合があります。
その場合、2種類の初期化方法があります。
ひとつはコンストラクタで引数を取り、さらに実行時に各種メンバ変数に代入する、という方法です。
class Hoge
{
public:
Hoge(int age, string name) {
this->age = age;
this->name = name;
}
int age;
string name;
}
// --------------------
// 宣言と実装を分ける
class Hoge
{
public:
Hoge(int age, string name);
int age;
string name;
}
Hoge::Hoge(int age, string name) {
this->age = age;
this->name = name;
}
もうひとつはコンストラクタの初期化時に初期化する方法です。
class Hoge
{
public:
Hoge(int age, string name) : age(age), name(name) {};
}
// --------------------
// 宣言と実装を分ける
class Hoge
{
public:
Hoge(int age, string name);
}
Hoge::Hoge(int age, string name) : age(age), name(name) {};
見て分かる通り、コロン:
に続けてメンバ変数に対してコンストラクタの引数で受け取った値を設定しています。
書式が最初「ん?」という感じですが、分かってしまえば単純です。
実行時に値を代入するよりも、後者の初期化を使ったほうがコストが低いらしいです。
親クラスのコンストラクタを実行する
継承先クラスで親クラスのコンストラクタを呼び出したいケースは多いでしょう。
その場合はメンバ変数の初期化と同じように親クラスに引数を渡してやります。
class Fuga : public Hoge
{
public:
Fuga(int age, string name) : Hoge(age, name) {};
}
対象のポインタがあるクラスかどうかを確認する
コンテナなどに入れた対象がとあるクラスを継承しているかを確認する方法です。
こちらの記事を参考にさせていただきました。
#include <iostream>
using namespace std;
class Hoge
{
public:
Hoge(int age, string name) : age(age), name(name) {};
int age;
string name;
virtual ~Hoge() {};
};
class Fuga : public Hoge
{
public:
Fuga(int age, string name) : Hoge(age, name) {
this->weight = 30;
};
~Fuga() {};
int weight;
};
int main() {
Hoge *hoge = new Hoge(15, "aaa");
Hoge *fuga = new Fuga(20, "bbb");
Fuga *test = dynamic_cast<Fuga*>(fuga);
// fugaが`Fuga`であれば通る
if (test) {
cout << test->weight << endl;
}
}
ポイントはdynamic_cast<T*>
を使っている点です。
test
変数を評価している部分がありますが、キャストできない場合はこのif文がスキップされます。
つまりキャストが成功すれば該当のポインタ変数は調べたいクラスであることが分かる、ということです。
picojsonを使ってJSONをパースする
C++の標準のライブラリにJSONをパースする「spine/Json.h」がありますが、どうやら遅いらしいのでpicojsonを使ってみました。
#include "picojson.h"
#include <sstream>
#include <fstream>
int main(int argc, char *argv[])
{
std::stringstream ss;
std::ifstream f;
f.open("hoge.json");
ss << f.rdbuf();
f.close();
picojson::value v;
picojson::parse(v, ss);
picojson::array arr = v.get<picojson::object>()["hoge"].get<picojson::array>();
int i, l = arr.size();
for (i = 0; i < l; i++) {
std::cout << arr[i].get<picojson::object>()["fuga"].to_str() << std::endl;
}
return 0;
}
コードを見てもらえればなんとなく分かると思います。
ファイルを開いてpicojson::value
型に内容を流し込みます。
あとはpicojson::object
やpicojson::array
などの型を指定してJSON内のオブジェクトを取得していきます。
CC_CALLBACK_1
とstd::bind
CC_CALLBACK_n
(cocos2d.hで定義されているマクロ)を使ってメニューなどのコールバックを受け取る仕組みがあります。
中身を見てみるとstd::bind
をラップするマクロになっています。
このstd::bind
がなんなのか、というと、間違いを恐れずに言うならJavaScriptのcall
やapply
の類でしょう。
少し前の記事ですがこちらの記事(std::bindをやっと理解)を読んで動作が分かりました。
std::bind
は第一引数に関数に類するもの(関数ポインタやラムダ式)を受け取り、引数を束縛(bind)することができます。
引数が複数あり、束縛したくない引数はstd::placeholders::_n
を使ってプレースホルダーを指定するようです。
void someFunc(int a, int b)
{
std::cout << a + b << std::endl;
}
std::function<void(int)> bindFunc = std::bind(someFunc, std::placeholders::_1, 5);
// 引数ひとつで、
bindFunc(10); // 15と出力
この仕組を利用して、クラスのメソッドをコールバックとして利用している、というわけです。
スレッドセーフな処理
C++でスレッドセーフな処理を行うにはstd::mutex
を利用して、以下のように処理を書きます。
ちなみに、mutex(ミューテックス)はMUTual EXclusion
(排他処理)の省略形らしいです。
#include <mutex>
static std::mutex s_mutex;
void hoge() {
std::lock_guard<std::mutex> lock(s_mutex);
// do something
}
作ったアプリたち
今回の「ゲームレース」では社内のメンバー4人で「誰が一番売り上げるか」というルールで勝負しましたw
作られたゲームたちを紹介します。(申請中のものもあるので、順次、公開され次第更新していく予定です)
8月頭から企画、実装を開始して9月に申請する、というルールでした。
1ヶ月という短い期間ながら、みんな面白いゲームを作ったのでよかったら遊んでみてください( *'-')
Swipe Defender
まずは自分から。
今回の「ゲームレース」で作ったゲームは「Swipe Defender」というゲームです。
スワイプでバリアを張って、隕石を打ち返すシ
すしとぼく
inon29さんの作品「すしとぼく」。シンプルながらかわいいイラストとポップな音が癖になります。
Android版。(iOSはただいま申請中)