iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど
cocos2d-x v2には、画面キャプチャの機能が用意されていないので、実装した。
ただし、内容には満足していない部分があって、例えば、使用しているOpenGL ESのAPIは呼び出しコストが高いという情報があるのと、フレームバッファの解像度が高ければ高いほどメモリを使用するので、機会があれば改善したい。
/*!
@brief Screenshotクラス
*/
class MyGrab {
public:
/*! @brief デストラクタ */
virtual ~MyGrab();
/*!
@brief Singletonなインスタンスを返す。マルチスレッドを考えるの、この実装では不受分なのは認識している。
@result BQGrabのインスタンス
*/
static MyGrab* getInstance();
/*!
@brief Screenshot
@result 画像
*/
cocos2d::CCImage* grab();
private:
/*!
@brief コンストラクタ
@discussion Singletonとする為、コンストラクタは非公開メンバ関数とする。
*/
MyGrab();
/*!
@brief 初期化。
@retval true 成功
@retval false 失敗
*/
bool initialize();
};
MyGrab::MyGrab()
{
/* 初期化 */
initialize();
}
MyGrab::~MyGrab()
{
}
MyGrab* MyGrab::getInstance()
{
static MyGrab grab;
return &grab;
}
bool MyGrab::initialize()
{
return true;
}
CCImage* MyGrab::grab()
{
/* EGLビューのフレーム・サイズを取得する */
const unsigned int width = (unsigned int)CCDirector::sharedDirector()->getOpenGLView()->getFrameSize().width;
const unsigned int height = (unsigned int)CCDirector::sharedDirector()->getOpenGLView()->getFrameSize().height;
const unsigned int bytesPerPixel = 4; /* RGBA */
GLubyte* dataBuffer = NULL;
dataBuffer = (GLubyte*)malloc(width * height * bytesPerPixel);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, dataBuffer);
GLubyte *imageBuffer = NULL;
imageBuffer = (GLubyte*)malloc(width * height * bytesPerPixel);
for (int i = 0; i < height; ++i) {
memcpy(&imageBuffer[i * width * 4], &dataBuffer[(height - i - 1) * width * 4], width * 4);
}
CCImage* image = new CCImage();
image->initWithImageData(imageBuffer, width * height * bytesPerPixel, CCImage::kFmtRawData, width, height, 8);
free(dataBuffer);
free(imageBuffer);
image->autorelease();
return image;
}
glReadPixels()はフレームバッファの内容をピクセルデータとして読み取るコマンドだ。
void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid *data);
フレームバッファからウィンドウ座標xとyを左下隅としてwidth × heightのピクセル矩形を読み込み、dataで指定されたアドレスに格納する。formatパラメータは、そのピクセルが何を表しているかをOpenGLに伝える。typeパラメータは、ピクセルのデータ型を示す。
iOSもAndroidも、OpenGL ESから次の新しい描画エンジンへの移行を進めている。
Androidについては、GoogleがVulkan上にOpenGL ESを実装して、OpenGL ESの提供は続くようなので安心なのだが、iOSについては、AppleがOpenGL ESを非推奨となっているので、今後、利用できなくなっても不思議ではない。
そこで、cocos2d-xを利用している場合が対応方法を調べてみた。
macOSとiOSでVulkanのAPIを提供するMoltenVKを提供したMoltenの製品で、Metal上にOpenGLとOpenGL ESのAPIを提供している。
MoltenVKは、Khronos Groupと共同でオープンソースとなっていて、cocosのフォーラムでも協力者がいればMoltenGLのオープンソース化も検討していそうだ。
ただ、MoltenGLで、問題なく、OpenGL ESを利用しているアプリケーションがMetal対応になるかは、やってみないとわからないと思う。
cocs2d-x v3.17から派生したmetal-supportブランチで、Metal対応が進んでいる。
GitHubのIssuesでのやり取りも活発なので、metal-supportブランチが安定して成長するよう、一緒に貢献していくというのもありだと思う。
自分はスパゲティ・プログラムには二種類あって、一つは未熟だから、もう一つは経験も技術もあるのに、と考えている。そして、問題は後者の方で、考えに考え抜いたスパゲティ、理由を聞いても皆が納得するないようで、本人も信念を持って取り組んだ結果だから、それを何とかするのが非常に困難だと考えている。
今回、別件でワインバーグ氏について調べていたら、スパゲティ・プログラムの原因を見つけるヒントが目に留まったので、それと、発見した原因を発表する。
『コンサルタントの秘密 - 技術アドバイスの人間学 -』で紹介されていて、生物学の分野でロナルド・フィッシャー卿の「フィッシャーの基本定理」と呼ばれるもので、自然淘汰に関する基本定理だ。
ワインバーグ氏の著書では、プログラミングに関して取り上げた定理でないが、この意味を考えてみた閃いた。
経験のある、プロジェクトでも声がでかい人間が生み出すスパゲッティ・プログラム。それは、今、この瞬間に最適なもので、そのため、将来の変化に対応できず、それが、のちの人を苦しめている。
具体的な例を挙げてみよう。
例えば、三つのテーブル・ビューがあるスマートフォン・アプリケーション。一つは新規リスト、もう一つは、編集リスト、最後は削除リスト。リストを管理して、表示するという部分のコードは共通化できる。ベースクラスを導入しよう、と、スパゲティ・プログラム料理人は考える。
class BaseTableViewController {
func sortList() {
....
}
}
よし、うまくいった。これがリファクタリングだ!俺ってすごい。
ここで悲しい依頼が。新規リストは、条件によって並び替えのルールを変えて欲しい。ああ、どうしよう。そうだ、前処理関数を導入すればいい!
class BaseTableViewController {
func prepare() {
...
}
func sortList() {
prepare()
...
}
}
class NewTableViewController {
override func prepare() {
...
}
}
新規リストのクラスで、前処理関数をオーバーライドして、挙動を変えればいい!
こうして、また新たなスパゲティ・プログラムが生み出される!
基本クラスのコードを読んでも、新キリストのコードを読んでも、どんな動きをするのか読み取れない。
では、どうやって、スパゲティ・プルグラムの調理が行われているのを見つけるのか?
長年の研究によって、自分が見つけたのは、以下だ。
NSUndoManagerの利用は、Swiftで楽になったと思うが、その仕組みが見えにくくなったと思うので、Objective-Cの場合から説明する。
CocoaのUndoとRedoは、NSInvocationというクラスでNSObjectの子クラスとメソッドを保持し、それをNSUndoManager内のスタックで管理することで実現している。
なんらかの操作を行うと、Undoに必要なNSInvocationのインスタンスがUndoスタックに積まれていく。
ユーザがUndoを行うと、Redoに必要なNSInvocationのインスタンスがRedoスタックに積まれていく。
Objective-Cのコードで、以下のようになる。
- (void)makeItHotter
{
temperature = temperature + 10;
[[undoManager prepareWithInvocationTarget:self] makeItColder];
}
- (void)makeItColder
{
temperature = temperature - 10;
[[undoManager prepareWithInvocationTarget:self] makeItHotter];
}
makeItHotterで温度を10度上げて、makeItColderをNSUndoManagerに積む。
makeItColderでは、温度を10度下げて、makeItHotterをNSUndoManagerに積む。
これをSwiftを書く場合、積むメソッドのselectorを用意するのが面倒になるのだが、selectorを必要としないメソッドが用意されていた。以下のようになる。
func makeItHotter() {
var temperature = self.textField.intValue
temperature = temperature + 10
self.undoManager?.registerUndo(withTarget: self, handler: {
vc in
vc.makeItColder()
})
self.textField.intValue = temperature
}
func makeItColder() {
var temperature = self.textField.intValue
temperature = temperature - 10
self.undoManager?.registerUndo(withTarget: self, handler: {
vc in
vc.makeItHotter()
})
self.textField.intValue = temperature
}