cocos2d-xのAndroidの画面自動回転

cocos2d-x現時点バージョン3.17でも、コマンドで生成したデフォルトプロジェクトでは、iOS版が何も変更せずに、デバイスを上下反転すると、画面の向きも自動的に回転してくれると思いますが、Android版では未だに画面の自動回転に対応していないようです。

実はAndroid版の対応方法も簡単です。

プロジェクトルート/proj.android/app/src/org/cocos2dx/cpp/AppActivity.javaを開き、onCreate()のところに以下のソース3行を追加するだけです。

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
        }

これだけです!

そしたらファイル全体がこんな感じになると思います。

package org.cocos2dx.cpp;

import android.os.Bundle;
import org.cocos2dx.lib.Cocos2dxActivity;

public class AppActivity extends Cocos2dxActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.setEnableVirtualButton(false);
        super.onCreate(savedInstanceState);
        // ここから
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
        }
        // ここまで
        // Workaround in https://stackoverflow.com/questions/16283079/re-launch-of-activity-on-home-button-but-only-the-first-time/16447508
        if (!isTaskRoot()) {
            // Android launched another instance of the root activity into an existing task
            //  so just quietly finish and go away, dropping the user back into the activity
            //  at the top of the stack (ie: the last state of this task)
            // Don't need to finish it again since it's finished in super.onCreate .
            return;
        }
        // DO OTHER INITIALIZATION BELOW
        
    }

}
参考
  1. Autorotation problem

cocos2d-x v3.17がAndroidでビルドできない時の対策

※2018/11/18更新

NDK Build

従来cocos2d-xのプロジェクトに自前のクラスを追加したら、iOSの場合はXcodeにファイルを追加すれば良いし、Androidの場合はproj.android/app/jni/Android.mkに相応のファイルを追加すれば良かったのですが、v3.17ではそう行かなくなったようです。

実験として、新規プロジェクトを作成:

cocos new Test1 -l cpp

そして、デフォルトのHelloWorldシーンをMainシーンに変えてみました。
iOSで動作確認の上、上記Android.mkにMainScene.cppを追加して以下にすれば良いはずです。

LOCAL_SRC_FILES := $(LOCAL_PATH)/hellocpp/main.cpp \
                   $(LOCAL_PATH)/../../../Classes/AppDelegate.cpp \
                   $(LOCAL_PATH)/../../../Classes/HelloWorldScene.cpp \
                   $(LOCAL_PATH)/../../../Classes/MainScene.cpp

ところが、実行すると以下のようなエラーが出ました。

$ cocos run -p android
...
  /Users/poo/test/Test1/Classes/AppDelegate.cpp:121: error: undefined reference to 'MainScene::createScene()'
  clang++: error: linker command failed with exit code 1 (use -v to see invocation)
  ninja: build stopped: subcommand failed.

Mainシーンのクラスファイルが見つからないようですね。

ログをよく見ると、最初に実行されたコマンドは以下でした。

running: '"/Users/poo/test/Test1/proj.android/gradlew" --parallel --info assembleDebug -PPROP_BUILD_TYPE=cmake'

最後のビルドタイプはcmakeになっていることが分かります。

調べたところ(参考1)、proj.android/gradle.propertiesの中でデフォルトPROP_BUILD_TYPE設定がcmakeになっているので、それをndk-buildに変えればAndroid.mkが参照されるようになっているとのことでした。

しかし実際いくら変えても、実行されるコマンドは”-PPROP_BUILD_TYPE=cmake”のままで、もちろんビルドも失敗で終わります。

そこまで言うならcmakeを使うよとすっかり負けてしまいました。Orz
※2018/11/18追記:


proj.android/gradle.propertiesの設定はAndroid Studioに有効であり、コマンドラインでは–build-typeの指定で解決できることが分かりました。
つまりndk-buildでビルドしたいなら、コマンドは以下になります。

$ cocos run -p android --build-type ndk-build

今回はこれが回答になるかと思います。
以下のCMake部分では新たに追加した参考3によると、Google NDKのガイドではCMakeをネイティブライブラリをビルドするデフォルトツールとされているため、今後はndk-buildが徐々に減り、CMakeが徐々に増える傾向となるでしょう。
なので、早いうち慣れとくといいと思います。

しかしSDKBOXを使うなら、現段階v1.0.2.0ではCMakeに関してはノータッチのようですので、インポートするとCMakeではコンパイルエラーになります。
設定に関する情報も探してみましたが、見つかったのが参考5ぐらいで、まだまだ情報が少ないです。
ちなみに、参考5の情報は既に古いようで、そのままやってもうまくいきませんでしたので、今のところSDKBOXを使うなら、やっぱりndk-buildに切り替えたほうが一番手っ取り早いだと思います。


CMake

CMakeのクラスファイルリストはプロジェクトルートにある「CMakeLists.txt」で設定できます。
デフォルトはこうなっているはずです。

list(APPEND GAME_SOURCE
     Classes/AppDelegate.cpp
     Classes/HelloWorldScene.cpp
     )
list(APPEND GAME_HEADER
     Classes/AppDelegate.h
     Classes/HelloWorldScene.h
     )

ここに追加したcppファイルとヘッダーをそれぞれ追加すれば良いはずです。

また、Classesディレクトリーの下にサブディレクトリーを作り、それらを追加したい場合、もうひと手間かかります。
例えば今回のMainSceneはClasses/mainのところに置くとします。
構造はこうなるはずです。

Classes/
├── AppDelegate.cpp
├── AppDelegate.h
├── HelloWorldScene.cpp
├── HelloWorldScene.h
└── main
    ├── MainScene.cpp
    └── MainScene.h

この場合、上記のcppとヘッダーファイル追加するだけでは足りません。
このようなエラーになるはずです。

  /Users/poo/test/Test1/Classes/AppDelegate.cpp:26:10: fatal error: 'MainScene.h' file not found
  #include "MainScene.h"
           ^~~~~~~~~~~~~
  1 error generated.

サブディレクトリーを作った場合、CMakeLists.txtの128行あたりにある「PRIVATE Classes」のあとにサブディレクトリーを追加する必要があります。

target_include_directories(${APP_NAME}
        PRIVATE Classes
        PRIVATE Classes/main #<=ここらへんに追加
        PRIVATE ${COCOS2DX_ROOT_PATH}/cocos/audio/include/
)

まとめ

新しいクラスファイル追加した場合、Android版ではCMakeLists.txtに以下の3箇所に追加する必要になります。

  1. list(APPEND GAME_SOURCE ブロックにCPPファイルを追加
  2. list(APPEND GAME_HEADER ブロックにヘッダーファイルを追加
  3. Classesの下にサブディレクトリーがあれば、target_include_directories(${APP_NAME} ブロックにそのディレクトリーを追加

ファイルがたくさんあるときは、以下のコマンドでリストアップしてからコピペするといいでしょう。

#cpp
find Classes -name *.cpp -type f

#header
find Classes -name *.h -type f
参考
  1. CMake Guide
  2. How to properly add include directories with CMake
  3. Android template project doesn’t build on `PROP_BUILD_TYPE=ndk-build`
  4. Can’t use cmake build for android when using cocos console
  5. SDKBOX CMake integration

Android v4.1~v4.4のTLS v1.2対応

さくらインターネットが5月でTLS1.0/1.1が使えなくなるため、cocos2d-xで作ったゲームがAndroid v4.1からv4.4(API Level 16~19)において通信ができなくなる問題が起きました。
SSLSocket参考

基本的には以下のところで紹介された方法でやりましたが、cocos2d-x周りに少し変更が必要でした。
Android 4.1+ enable TLS 1.1 and TLS 1.2
ソースコードはこちらです。

今回対応が必要としたcocos2d-xのバージョンは3.8.1です。

  1. まずは以下の内容でTLSSocketFactory.javaファイルを作ります。
    package org.cocos2dx.lib;
    
    import java.io.IOException;
    import java.net.InetAddress;
    import java.net.Socket;
    import java.net.UnknownHostException;
    import java.security.KeyManagementException;
    import java.security.NoSuchAlgorithmException;
    
    import javax.net.ssl.SSLContext;
    import javax.net.ssl.SSLSocket;
    import javax.net.ssl.SSLSocketFactory;
    
    /**
     * @author fkrauthan
     */
    public class TLSSocketFactory extends SSLSocketFactory {
    
        private SSLSocketFactory internalSSLSocketFactory;
    
        public TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException {
            SSLContext context = SSLContext.getInstance("TLS");
            context.init(null, null, null);
            internalSSLSocketFactory = context.getSocketFactory();
        }
    
        public TLSSocketFactory(SSLSocketFactory sslSocketFactory) throws KeyManagementException, NoSuchAlgorithmException {
            internalSSLSocketFactory = sslSocketFactory;
        }
    
        @Override
        public String[] getDefaultCipherSuites() {
            return internalSSLSocketFactory.getDefaultCipherSuites();
        }
    
        @Override
        public String[] getSupportedCipherSuites() {
            return internalSSLSocketFactory.getSupportedCipherSuites();
        }
        
        @Override
        public Socket createSocket() throws IOException {
            return enableTLSOnSocket(internalSSLSocketFactory.createSocket());
        }
    
        @Override
        public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
            return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));
        }
    
        @Override
        public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
            return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
        }
    
        @Override
        public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
            return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));
        }
    
        @Override
        public Socket createSocket(InetAddress host, int port) throws IOException {
            return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
        }
    
        @Override
        public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
            return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
        }
    
        private Socket enableTLSOnSocket(Socket socket) {
            if(socket != null && (socket instanceof SSLSocket)) {
                ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.2"});
            }
            return socket;
        }
    }
    

    ここ変えたのは最初のパッケージ名と最後のTLSv1.1を消すぐらいです。さくらインターネットはTLSv1.1も無効にするので不要かと。

  2. 上記TLSSocketFactory.javaをcocos2dxプロジェクトルート/cocos2d/cocos/platform/android/java/src/org/cocos2dxに保存します。
  3. /cocos2d/cocos/platform/android/java/src/org/cocos2dx/lib/Cocos2dxHttpURLConnection.javaを変更します。
    1. 54行あたりに以下を追加
      import org.cocos2dx.lib.TLSSocketFactory;
      import javax.net.ssl.SSLSocketFactory;
      
    2. 131行あたりに以下のをコメントアウト
            SSLContext context = SSLContext.getInstance("TLS");
            context.init(null, tmf.getTrustManagers(), null);
      
            httpsURLConnection.setSSLSocketFactory(context.getSocketFactory());
      
    3. 上記コメントアウトの箇所すぐ下に以下のを追加
                  SSLSocketFactory noSSLv3Factory = new TLSSocketFactory();
                  httpsURLConnection.setSSLSocketFactory(noSSLv3Factory);
      
  4. 本来ここまでやれば良いはずですが、cocosはssl認証のような面倒なことはやってくれませんでした。つまり、通常HTTPS通信行う時、上記変えた箇所のsetVerifySSL(HttpURLConnection urlConnection, String sslFilename)メソッドには通っていないんです。
    そのため、こちらのSSL証明書をチェックさせる方法で無理に通らせることにしました。
    HttpRequestを作る前に以下を追加しました。

        auto path = FileUtils::getInstance()->fullPathForFilename("cacert.pem");
        HttpClient::getInstance()->setSSLVerification(path);
    

    ちなみに、このcacert.pemはどこから来たかというと、自分サイトから以下のコマンド実行すると得られます。

    openssl s_client -showcerts -connect ホスト名:443 </dev/null 2>/dev/null|openssl x509 -outform PEM >cacert.pem
    

    そしてこのpemファイルをresディレクトリなどの場所に置き、Xcodeのプロジェクトにも追加すれば良いでしょう。

    How to save a remote server SSL certificate locally as a fileのelec3647の回答
    余談になりますが、実際全く違うサイトの証明書を落としてきて試したところ、ここのca証明書なんでもいいようです。
    /cocos2d/cocos/network/HttpClient-android.cppの541行あたりのsetVerifySSL()を見ると分かりますが、まずはssl caファイルをチェックし、それがなければ狙いのsetVerifySSLに通してくれないので、あまり意味がなくても設定します。
    ※あっ!もしかして、そのif(_client->getSSLVerification().empty()) returnをコメントアウトする手もあったでは?!

  5. 最後に/cocos2d/cocos/network/HttpAsynConnection-apple.mの193行あたりに立て続け[(id)certArrayRef release];が2つありますので、そのうちの一つをコメントアウト(削除)します。
    ここでもcocosのssl証明書に関するやる気のなさが窺えます。

これでAndroid v4.1~v4.4の端末でも問題なくHttpRequestで通信できるはずです。

お気づきの方もいらっしゃると思いますが、Cocos2dxHttpURLConnection.javaのsetVerifySSL(HttpURLConnection urlConnection, String sslFilename)メソッド中では、SSLContext作る前に証明書やキーなどいろいろと準備してましたが、今回の変更で全く使われなくなります。

こちらのgotevが紹介した方法でやりたくなります(実際やりたくなりました^^;)が、下記のエラーが出てしまいます。

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

ここは深入りしてませんが、つまりtmf.getTrustManagers()を渡してcontext.init(null, tmf.getTrustManagers(), null);するとパスが見つからないというエラーが出ますが、context.init(null, null, null);にするといいです。
この問題はstevieが言ってることがを参考にするといいかもしれません。