Unreal iOS开

  • 可以Object-C与C++混合编程
     相对以Android开发,OC会更简单。在Android平台接入SDK还需要通过UPL作为桥接去访问java函数或者实现java的native函数。但在iOS一切都因为混编变得简单。
我们来看看下面获取appDocuments目录的一段代码,并在UE的Log中将其输出:

ELOG就是UE_LOG的一个扩展。
#if PLATFORM_IOS
char buffer[256]={0};
snprintf(buffer, sizeof(buffer), "%s%s", getenv("HOME"),"/Documents");
FString DestPath = UTF8_TO_TCHAR(buffer);
ELOG(TEXT("Env Documents Path is: %s"), *DestPath);
NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
DestPath = UTF8_TO_TCHAR([documentPath UTF8String]);
ELOG(TEXT("OC Documents Path is: %s"), *DestPath);
#else
FString DestPath = FPaths::ProjectPersistentDownloadDir();
#endif
这段代码是写在一个蓝图可访问的函数体内。可以看到有两种风格的代码:
第一种:如果平台为非IOS,为正常的Unreal C++代码,获取PersistentDownloadDir。
第二种:如果是IOS平台。更离谱,一会是Unreal C++代码(FString DestPath = UTF8_TO_TCHAR("")),一会是OC代码(NSString *documentPath = ....)
这就是我们所说的混编,是不是感觉非常酷。

  • 项目文件结构
    既然可以在UE C++代码中嵌入OC代码,可不可以将已经写好的类直接在代码中使用呢,将所有的OC代码都封装起来?
    答案是肯定的。接下来将以插件的方式讲解如何将OC代码在UE中使用起来。下图展示的是在一个名为MySqlite3的插件中的文件结构
在include文件夹中放有sqlite的头文件。在lib中放有iOS平台sqlite3静态库。在public目录下是OC封装的SqliteMgr。
  • 插件Build.cs配置
    ModuleDirectory所指向的路径就是Build.cs所在路径,后面的配置都相对于该路径进行配置。
    PublicIncludePaths添加需要到include路径到项目配置中。不然编译时不会去这里找.h。有public就有private,就跟他们的名字一样,是插件自己使用还是公开给引用它们的模块使用。
从图中可以看到,我们将include目录和Public/iOS目录都添加到查找路径中。
    PublicAdditionalLibraries添加静态库到项目路径。
    PrivateDependencyModuleNames添加本模块(插件)需要依赖的模块。
    PublicFrameworks可以添加iOS的framework。在本插件中添加了Foundation。在后面的SqliteMgr会用到里面的内容。

  • 标准的Object-C类
    如前面项目结构图所示,开发一个标准的OC类,包含头文件和.mm。下面是部分代码:

#include <Foundation/Foundation.h>
#include <sqlite3.h>
@interface SqliteMgr : NSObject {
sqlite3 *_db;
}
/**
* 获取单例
* @return 单利
*/
+ (instancetype)getInstance;
/**
* 打开指定的数据库
* @param db 数据库路径
* @return 是否打败成功
* BOOL
*/
- (BOOL)openDB:(NSString *)db;
/**
* 执行SQL
* @param sql
* SQL语句
* @return
* BOOL
*/
- (BOOL)exec:(NSString *)sql;
/**
* 查询SQL
* @param sql 查询SQL
* @return 返回一个数组代表数据行
*/
- (NSArray *)query:(NSString *)sql;
/**
* 查询第一条
* @param sql 查询第一条数据
* @return Key/Value
*/
- (NSDictionary *)queryFirst:(NSString *)sql;
/**
* 统计
* @param sql 统计数据的sql
* @return 数据总数
*/
- (double)count:(NSString *)sql;
/**
* 关闭链接
* @return Sqlite错误码
*/
- (int)close;
@end

#include <Foundation/Foundation.h>

#include "SqliteMgr.h"
@implementation SqliteMgr {
}
static SqliteMgr *instance;
+ (instancetype)getInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
/**
* 打开数据库
* @param db
* @return
*/
- (BOOL)openDB:(NSString *)db {
NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *DBPath = [documentPath stringByAppendingPathComponent:db];
NSLog(@"数据库路径%@", DBPath);
if (sqlite3_open(DBPath.UTF8String, &_db) != SQLITE_OK) {
return NO;
} else {
return YES; //打开成功创建表
}
}
/**
* 执行SQL
* @param sql
* SQL语句
* @return
* bool
*/
- (BOOL)exec:(NSString *)sql {
return sqlite3_exec(_db, sql.UTF8String, nil, nil, nil) == SQLITE_OK;
}
/**
* 查询语句
* @param sql
* SQL语句
* @return
* 结果集
*/
- (NSArray *)query:(NSString *)sql {
sqlite3_stmt *pstmt; //结果集游标句柄
if (sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, NULL) != SQLITE_OK) {
NSLog(@"执行SQL失败%@", sql);
return NULL;
}//查询成功
NSMutableArray *dicArr = [[NSMutableArray alloc] init];
while (sqlite3_step(pstmt) == SQLITE_ROW) { //遍历游标
[dicArr addObject:[self stmt2Dict:pstmt]];
}
sqlite3_finalize(pstmt);
return dicArr;
}
/**
* 查询第一个结果
* @param sql
* @return
*/
- (NSDictionary *)queryFirst:(NSString *)sql {
sqlite3_stmt *pstmt; //结果集游标句柄
if (sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, NULL) != SQLITE_OK) {
NSLog(@"执行SQL失败%@", sql);
return NULL;
}//查询成功
while (sqlite3_step(pstmt) == SQLITE_ROW) { //遍历游标
return [self stmt2Dict:pstmt];
}
return nil;
}
/**
* sqlite3_stmt 转成 Dictionary
* @param pstmt
* sqlite3_stmt 指针
* @return
* Dictionary
*/
- (NSDictionary *)stmt2Dict:(sqlite3_stmt *)pstmt {
int columnCount = sqlite3_column_count(pstmt); //获取列数
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; //初始化一个Dict存放单行记录
for (int i = 0; i < columnCount; i++) {
const char *key = sqlite3_column_name(pstmt, i); //取列名
int type = sqlite3_column_type(pstmt, i);
switch (type) {
case SQLITE_INTEGER: {
int value = sqlite3_column_int(pstmt, i); //整型
dict[[NSString stringWithUTF8String:key]] = @(value);
break;
}
case SQLITE_FLOAT: {
double value = sqlite3_column_double(pstmt, i); //浮点型
dict[[NSString stringWithUTF8String:key]] = @(value);
break;
}
case SQLITE_TEXT:
default: {
const char *value = (const char *)(sqlite3_column_text(pstmt, i)); //字符型
dict[[NSString stringWithUTF8String:key]] = [NSString stringWithUTF8String:value];
break;
}
}
}
return dict;
}
/**
* 执行统计类型语句,例如 COUNT(1) SUM(col),返回第一个记录
* @param sql
* SQL语句
* @return
* count
*/
- (double)count:(NSString *)sql {
sqlite3_stmt *pstmt; //结果集游标句柄
if (sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, NULL) != SQLITE_OK) {
NSLog(@"执行SQL失败%@", sql);
return 0;
}
while (sqlite3_step(pstmt) == SQLITE_ROW) {
return sqlite3_column_double(pstmt, 0);
}
return 0;
}
/**
* 关闭链接
* @return
*/
- (int)close {
return sqlite3_close(_db);
}
@end
  • 利用混编使用该单例类
    如下图所示,使用混合开发,使用前面的类。有了混编的优势,可以灵活的插入C++代码和OC代码,将OC操作的数据给到UE使用。这里是将OC查询到的数据在UE log中打印出来。

  • invalid argument -std= c++14错误
如果过后缀名为.m,打包时可能会报错:invalid argument -std=c++14 该参数可以在IOSToolChain.cs中找到,对于CPP、MM、M、C、PCH都有不同的参数。可以将文件改为.mm
  • [-Werror,-Wdeprecated-declarations]错误
    错误详情:
lib\iOS\IFlyMSC.framework\Headers\IFlyPcmRecorder.h:58:39: error: 'AVAudioSessionDelegate' is deprecated: first deprecated in iOS 6.0 - No longer supported [-Werror,-Wdeprecated-declarations]
UATHelper: Packaging (iOS):     @interface IFlyPcmRecorder : NSObject<AVAudioSessionDelegate>
UATHelper: Packaging (iOS):                                           ^
UATHelper: Packaging (iOS):     /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.2.sdk/System/Library/Frameworks/AVFoundation.framework/Frameworks/AVFAudio.framework/Headers/AVAudioSession.h:957:11: note: 'AVAudioSessionDelegate' has been explicitly marked deprecated here
UATHelper: Packaging (iOS):     @protocol AVAudioSessionDelegate <NSObject>
    解决办法同前面所讲,在IOSToolChain.cs中增加Result += " -Wno-deprecated-declarations";参数。

  • ld: framework not found libz.tbd(ld: framework not found libc++.tbd
UATHelper: Packaging (iOS):     ld: framework not found libz.tbd
UATHelper: Packaging (iOS):     clang: error: linker command failed with exit code 1 (use -v to see invocation)
PackagingResults: Error: linker command failed with exit code 1 (use -v to see invocation)
    添加依赖库时,不要把这种tbd添加到 PublicFrameworks,默认可以不添加,如果需要,将其添加到 PublicAdditionalLibraries

  • 包打出来之后校验失败导致无法安装
    由于在包含第三方framework时参数错误导致。第三参数和第四参数不要传。
PublicAdditionalFrameworks.Add( 
    new Framework( 
    "iflyMSC",
    MyPath + "/lib/iOS/iflyMSC.framework.zip"
    )
);

  • 科大讯飞SDK 开始监听之后,程序闪退

    闪退查起来比较困难,更困难的是现在开发的iOS插件。但方法都一样,首先尝试一下几种方法找到蛛丝马迹:
1、尝试找到Log
2、用经典的插拔法或者二分定位法
通过一系列的分析,找到了问题就出在库函数中,根本问题在于plist权限增加失败。(UPL文件并没有生效)
AdditionalPropertiesForReceipt.Add("IOSPlugin", "XFIosIat_UPL.xml");红色部分注意大小写。

<Error>: This app has crashed because it attempted to access privacy-sensitive data without a usage description.  The app's Info.plist must contain an NSMicrophoneUsageDescription key with a string value explaining to the user how the app uses this data.
在iPhone上怎么找到log呢?
这里的log分两种,一种是Unreal的log,一种是iphone的log(这与Android是一样的。)
Unreal Log在https://www.bilibili.com/read/cv21206910中有介绍。
iPhone Log可以通过iTools获取