Jennal's Blog https://jennal.com Jennal的技术博客 Fri, 19 Mar 2021 07:59:32 +0000 en-US hourly 1 https://wordpress.org/?v=5.3.2 Google Play上线的游戏,C#服务端如何校验内购订单 https://jennal.com/2021/03/19/google-androidpublisher-with-csharp/ https://jennal.com/2021/03/19/google-androidpublisher-with-csharp/#respond Fri, 19 Mar 2021 07:59:06 +0000 https://jennal.com/?p=849 准备工作
  1. 在Google Play Console中启用API权限
  2. 在Google Cloud中准备好服务账号
  3. 坑:如果游戏内购商品早于服务账号创建,需要修改一下内购商品的信息,更新一下,才能真的获得权限

1. 在Google Play Console中启用API权限

打开Google Play Console后台的首页(注意:不是某个游戏管理的首页)。

如果你是第一次打开这个页面,会提示你是否启用。点击启用按钮,会问你是否绑定Google Cloud、Google Developer等等,一路确定就好了。

2. 在Google Cloud中准备服务账号

然后点击 创建新的服务账号,根据指引进入 Google Cloud 的后台进行创建,名字取好,其他都默认就可以了。

创建完以后,很重要的一步,是要创建密钥。

选择默认的json密钥就可以了。点击创建以后,会自动下载一个json文件。这个json文件待会在代码中会用到。这个文件就是这个账号的密钥了,一定要保存好,因为之后就没有机会让你再下载第二次了。

这个时候返回 Google Play Console 刚刚的页面,刷新一下服务账号,就可以看到刚刚创建的服务账号了。然后根据你自己的需要对这个账号进行授权,就可以了。

3. 没有权限的问题解决

如果做完了以上两步,直接进行下面的代码测试,发现返回错误

The current user has insufficient permissions to perform the requested operation.

那恭喜你,遇到了Google的坑。这是由于你的服务账号创建以及绑定权限晚于内购商品的创建造成的。不过问题也比较容易解决,只要修改一下内购商品的信息,更新一下,就能真的获得权限了。

代码以及测试

API

我先找到了这个API文档,
https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products

它一上来就告诉我,会返回什么值,但是就是不告诉我怎么发起请求。

{
  "kind": string,
  "purchaseTimeMillis": string,
  "purchaseState": integer,
  "consumptionState": integer,
  "developerPayload": string,
  "orderId": string,
  "purchaseType": integer,
  "acknowledgementState": integer,
  "purchaseToken": string,
  "productId": string,
  "quantity": integer,
  "obfuscatedExternalAccountId": string,
  "obfuscatedExternalProfileId": string,
  "regionCode": string
}

于是我又找到了Google Api的C#封装:https://github.com/google/google-api-dotnet-client

并且提供了nuget支持:https://www.nuget.org/packages/Google.Apis.AndroidPublisher.v3

那一切就好办了

代码

private static Task<ProductPurchase> GetInAppPurchase(string packageName, string productId, string purchaseToken)
{
    GoogleCredential credential;
    //这里用到上面准备工作中得到的json
    using (Stream stream = new FileStream(@"api.json", FileMode.Open, FileAccess.Read, FileShare.Read))
    {
        credential = GoogleCredential.FromStream(stream);
    }

    string[] scopes = {
        AndroidPublisherService.Scope.Androidpublisher
    };
    credential = credential.CreateScoped(scopes);

    BaseClientService.Initializer initializer = new BaseClientService.Initializer()
    {
        HttpClientInitializer = credential,
        ApplicationName       = "My Application"
    };

    AndroidPublisherService service = new AndroidPublisherService(initializer);
    return service.Purchases.Products.Get(packageName, productId, purchaseToken).ExecuteAsync();
}

private static void TestGooglePlayPurchaseApi()
{
    //以下参数替换为自己项目对应的值
    string packageName = "validPackageName";          //包名
    string productId   = "validOrInvalidProductName"; //内购商品ID
    string purchaseToken = "validPurchaseToken";      //订单的PurchaseToken

    ProductPurchase purchase = GetInAppPurchase(packageName, productId, purchaseToken).Result;

    Console.WriteLine("Kind: " + purchase.Kind);
    Console.WriteLine("Quantity: " + purchase.Quantity);
    Console.WriteLine("AcknowledgementState: " + purchase.AcknowledgementState!);
    Console.WriteLine("ConsumptionState: " + purchase.ConsumptionState!);
    Console.WriteLine("DeveloperPayload: " + purchase.DeveloperPayload);
    Console.WriteLine("ETag: " + purchase.ETag);
    Console.WriteLine("OrderId: " + purchase.OrderId);
    Console.WriteLine("ProductId: " + purchase.ProductId);
    Console.WriteLine("PurchaseState: " + purchase.PurchaseState!);
    Console.WriteLine("PurchaseToken: " + purchase.PurchaseToken);
    Console.WriteLine("PurchaseType: " + purchase.PurchaseType!);
    Console.WriteLine("RegionCode: " + purchase.RegionCode);
    Console.WriteLine("PurchaseTimeMillis: " + purchase.PurchaseTimeMillis);
    Console.WriteLine("ObfuscatedExternalAccountId: " + purchase.ObfuscatedExternalAccountId);
    Console.WriteLine("ObfuscatedExternalProfileId: " + purchase.ObfuscatedExternalProfileId);
}

测试

包名和商品ID都是固定的,所以我们只要拿到一个 PurchaseToken 就可以进行测试了。如果你以为测试一定要集成到app中,才能得到 PurchaseToken,那就错了。其实我们在Google Play Console的后台,就能获得这个 PurchaseToken 了。打开Console默认页(非单款游戏管理页面),左边的菜单中,有个 订单管理,进入 订单管理,选择你想测试的游戏的订单,点击箭头,进入订单详情,在详情中,就可以复制 PurchaseToken 进行测试了。

参考

  1. https://stackoverflow.com/questions/64217553/purchases-products-get-is-ignoring-productid-value-and-returns-null-in-productpu
  2. https://stackoverflow.com/questions/43536904/google-play-developer-api-the-current-user-has-insufficient-permissions-to-pe
]]>
https://jennal.com/2021/03/19/google-androidpublisher-with-csharp/feed/ 0
C#中使用Pomelo进行MySql的原生Json查询 https://jennal.com/2021/02/02/csharp-linq-query-json-with-pomelo/ https://jennal.com/2021/02/02/csharp-linq-query-json-with-pomelo/#respond Tue, 02 Feb 2021 05:28:19 +0000 https://jennal.com/?p=848 Mysql已经原生支持json,有5年多了。但我还没有用过,搜了一圈,MySql本身的语法还是很好写的

mysql> SELECT c, JSON_EXTRACT(c, "$.id"), g
     > FROM jemp
     > WHERE JSON_EXTRACT(c, "$.id") > 1
     > ORDER BY JSON_EXTRACT(c, "$.name");
+-------------------------------+-----------+------+
| c                             | c->"$.id" | g    |
+-------------------------------+-----------+------+
| {"id": "3", "name": "Barney"} | "3"       |    3 |
| {"id": "4", "name": "Betty"}  | "4"       |    4 |
| {"id": "2", "name": "Wilma"}  | "2"       |    2 |
+-------------------------------+-----------+------+
3 rows in set (0.00 sec)

mysql> SELECT c, c->"$.id", g
     > FROM jemp
     > WHERE c->"$.id" > 1
     > ORDER BY c->"$.name";
+-------------------------------+-----------+------+
| c                             | c->"$.id" | g    |
+-------------------------------+-----------+------+
| {"id": "3", "name": "Barney"} | "3"       |    3 |
| {"id": "4", "name": "Betty"}  | "4"       |    4 |
| {"id": "2", "name": "Wilma"}  | "2"       |    2 |
+-------------------------------+-----------+------+
3 rows in set (0.00 sec)

但是有些语言偏偏搞了很复杂的封装,比如C#。我做过的项目,主要用Pomelo库来连接MySql。而Pomelo库曾经做了一套JsonObject的方案来使用MySql的json,后来又弃用了。但是网上大量留下了JsonObject的教程,这是多么尴尬的局面。最尴尬的是Pomelo库本身并没有编写完整的文档,所以能搜到的信息,基本都是误导信息。经过一番探索,我总结一下目前正统的方案。

方案开始

首先安装必要的库,我这里使用的是Microsoft的Json序列化方案,因为它支持递归引用的序列化,所以更推荐用它来做序列化。

dotnet add package Microsoft.EntityFrameworkCore.Design --version 3.1.10
dotnet add package Microsoft.EntityFrameworkCore.Relational --version 3.1.10
dotnet add package Pomelo.EntityFrameworkCore.MySql --version 3.2.4
dotnet add package Pomelo.EntityFrameworkCore.MySql.Json.Microsoft --version 3.2.4

经过验证,以下代码是无意义的,并且可能导致运行时异常。
然后需要在Startup.cs中的ConfigureServices,添加以下代码

//services.AddEntityFrameworkMySqlJsonMicrosoft();

完整示例

public void ConfigureServices(IServiceCollection services)
{
    var conn = Configuration.GetConnectionString("Mysql");
    Console.WriteLine(conn);
    services.AddDbContext<piContext>(options => 
        options.UseMySql(conn, x => x.ServerVersion("5.7.26-mysql")));
    //services.AddEntityFrameworkMySqlJsonMicrosoft();
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    services.AddControllers()
            .AddNewtonsoftJson();
}

最后,在需要使用json查询的位置,使用

var id = 1;
var result = dbContext.JsonTable.Where(o => EF.Functions.JsonExtract<int>(o.Json, "$.id") == id);

就能生成这样的Sql代码

select * from JsonTable where JSON_EXTRACT(Json, "$.id") = 1;

参考

]]>
https://jennal.com/2021/02/02/csharp-linq-query-json-with-pomelo/feed/ 0
Unity2018 设置 Android 程序名称的多语言 https://jennal.com/2021/01/06/localize-android-appname-on-unity-2018/ https://jennal.com/2021/01/06/localize-android-appname-on-unity-2018/#respond Wed, 06 Jan 2021 06:39:38 +0000 https://jennal.com/?p=845 Unity2018 设置 Android 程序名称的多语言

如果了解Android开发的话,应该很清楚Android的App名称,是可以多语言的,因为一般会在AndroidManifest.xml中,有如下配置

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.veewo.alienland">
  ...
  <application android:icon="@drawable/app_icon" android:label="@string/app_name" android:hardwareAccelerated="true">
    ...
  </application>
</manifest>

其中 android:label="@string/app_name" 就是设置App在Android桌面显示的名称。而这里使用的 @string/app_name 代表,这个名称,是设置在 res/values/strings.xml 中的。res/values/strings.xml 中的内容如下

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="app_name">This is a App</string>
</resources>

而我们可以很方便的进行多语言的配置,就是新建对应各种语言的 values 目录,例如:英文对应 values-en,中文对应 values-zh。甚至可以更精细地设置大陆地区使用简体中文 values-zh-rCN,台湾地区使用繁体中文 values-zh-rTW。只需要修改这些目录下对应的 strings.xml 的内容,就可以在不同语言的系统下,显示不同的内容。

由于Unity中Android对应的文件,都放在 Plugins/Android 目录下,并且看了一些文章,也都说在这个目录下建立一个 res 目录,然后把 values 目录放在 res 目录下就可以了。但是经过测试,发现这样并不生效。

然后又找到一篇Unity官方问答平台的回答 Overriding App icon string for Android builds,里面说 values-en 目录应该这样命名 values-b+en。我用这个方案测试了,一样不行。

最后,我不直接打包了,而是 Export Project,才发现了问题。导出的目录结构是这样的

.
 \ProjectName.iml
 \build
 \build.gradle
 \gradle
 \gradle.properties
 \gradlew
 \gradlew.bat
 \libs
 \local.properties
 \proguard-unity.txt
 \settings.gradle
 \src
  \main
   \assets
   \java
   \jniLibs
   \res
   \AndroidManifest.xml
 \unity-android-resources
  \AndroidManifest.xml
  \build
  \build.gradle
  \project.properties
  \res
  \unity-android-resources.iml

我期望 Plugins/Android/res 目录中的内容被拷贝到 src/main/res,而实际上,内容被拷贝到了 unity-android-resources/res 中。 其中 unity-android-resources 是一个Module的存在,所以代码是可以被正常访问的,但是我需要 values 被放到主项目的目录下。

Unity是支持自定义 gradle 脚本模板的,于是我想在 build.gradle 上做文章。

  1. 进入 Player Settings/Android/Publishing Settings ,把 Custom Gradle Template 勾起来。此时,Unity会自动生成文件 Assets/Plugins/Android/mainTemplate.gradle
  2. 打开 mainTemplate.gradle 文件,在末尾添加

task localizeAppName(type: Copy) {
    from(&quot;${project.rootDir}/unity-android-resources/res/&quot;) {
        include &quot;**&quot;
    }
    into &quot;${project.rootDir}/src/main/res&quot;
}

preBuild.dependsOn(localizeAppName)

大功告成。

]]>
https://jennal.com/2021/01/06/localize-android-appname-on-unity-2018/feed/ 0
在WinForm项目中使用Windows Runtime的方法 https://jennal.com/2016/04/28/using-windows-runtime-at-winform/ https://jennal.com/2016/04/28/using-windows-runtime-at-winform/#comments Thu, 28 Apr 2016 12:16:11 +0000 http://jennal.com/?p=827 在WinForm项目中使用Windows Runtime的方法

最近需要在Winform项目中使用蓝牙,蓝牙模块是同事负责的,找了好多版本的蓝牙库,对BLE的支持都不好。最后发现系统直接提供了Windows.Devices.Bluetooth这个库可以用,但是只能在Universal项目中使用。试过在nuget中找到的Target.WindowsRuntime,但是根本不能用。经过一番google,发现可以用hack的方法在Winform中使用,特此记录。

PS: 我用的是vs2015,win10,.net 4.5,据说win8 vs2013也是可以的,我没有测试过。如果使用其他版本的操作系统,或者.net版本,请自行修改对应参数测试。

步骤说明

  1. 手工修改csproj项目文件
  2. 添加对Windows.XXXXX库的引用
  3. 添加project.json配置文件
  4. 添加对WindowsRuntime库的引用

修改项目文件

需要关闭项目工程文件,手工在目标csproj文件中添加如下代码

<propertyGroup>
  <targetPlatformVersion>10.0</targetPlatformVersion>
</propertyGroup>

你期望编译的目标操作系统是win10,就写10.0,如果是win8,就写8.0,以此类推。

上个截图,更容易理解

添加对库的引用

这个时候,启动sln工程文件,然后右键点击引用-添加引用...,会发现,左侧的分类,多了一类Universal Windows

赶紧把需要的库加进来吧,加进来以后,发现代码中可以正常引用了。

但是会编译不过,提示

Error occurred while restoring NuGet packages: Could not find file 'C:\Users\user\Documents\Visual Studio 2015\Projects\TestWindowsRuntimeTarget\TestWindowsRuntimeTarget\project.json'.

添加project.json文件

上面的错误,提示我们需要project.json,在项目中新建这个名称的json文件,然后复制下面的内容

{
  "frameworks": {
    ".NETFramework,Version=v4.5": {
      "dependencies": {}
    }
  },
  "runtimes": {
    "win": {}
  }
}

其中的v4.5可以改成任意你需要的.net版本号。

再编译一次试试,大功告成,这样就可以顺利编译通过了。

添加对WindowsRuntime库的引用

这个时候虽然编译通过了,但是实际使用Windows相关类库的时候,还是会有问题,需要做最后一步操作来解决这个问题。

继续添加引用,并选择从文件添加,在下面的目录中,找到System.Runtime.WindowsRuntime.dll,并加入引用。

C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5

如果需要用到async/await,还需要添加对Windows.winmd的引用,在下面的目录中

C:\Program Files (x86)\Windows Kits\10\UnionMetadata

这样,就可以在WinForm项目中使用Universal的类库啦。

参考资料

]]>
https://jennal.com/2016/04/28/using-windows-runtime-at-winform/feed/ 10
CocosBuilder的Cocos2dx-lua绑定方案 https://jennal.com/2015/01/30/cocosbuilder-lua/ https://jennal.com/2015/01/30/cocosbuilder-lua/#comments Fri, 30 Jan 2015 09:35:29 +0000 http://jennal.com/?p=819 CocosBuilder的Cocos2dx-lua绑定方案

前情提要

似乎写每一份代码,每一篇文章,都有些理由,所以忍不住要写个前情提要。

CocosBuilder虽然已经2年没有更新了,已经被SpriteBuilder所取代,但是由于我们早期的项目使用的就是CocosBuilder,而SpriteBuilder增加的功能并没有特别出彩,没有足够的理由让我们抛弃CocosBuilder,于是沿用下来。但是如果拿不开源的CocosStudio来比较,我还是推荐使用CocosBuilder

据我所知,最早让Cocos2dx的lua版本支持CocosBuilder的,是这个项目:LuaProxy。不知道为什么,CocosBuilder官方库只做了对js的支持,甚至做了html5的支持,却完全没有考虑对lua的支持。而LuaProxy就是建立在官方对js支持的基础上,做了lua的实现。但是读完CocosBuilder中js方案的实现,以及Cocos2dxCCBReader对js方案的实现,我居然产生了,这不可能是一个人写的吧,这种想法。因为js方案与cpp方案的思路完全不同,如果是同一个人做的,这也太蛋疼了吧?(不过我没有看js那边的使用方法,只是按照LuaProxy的使用方法去理解的。)

cpp方案用了一种非常优雅的方式,加载的过程中,自定义类需要有一个对应的Loader,使用字符串来索引Loader,以帮助CCBReader生成对应的类的实例。

LuaProxy的方案的缺陷

  1. 首先最不能忍的,就是用了一个全局table ccb来替代cpp中的registerCCNodeLoader,看起来好像可以解决,但是必须事先把所有需要绑定的Controller的名字写入这个ccb里面,也就意味着很可能许多不需要的代码会被提前加载,对于lua这样的脚本语言来说,这完全是不必要的代价。
  2. 全局ccb也带来了另外一个问题,同屏加载多个相同ccbi时,动态更新数据的问题。
    • 试想这种情况:你用CocosBuilder的粒子编辑器做了一束烟花,但是粒子贴图要在几种里面随机,这个需要在代码里面实现。于是你把这个CCParticleSystemQuad绑定到了ccb的一个table中。然后需求变了,策划需要同时放多个烟花,于是同时出现的烟花,只有最后一个的贴图可以随机了。因为他们共享同一个全局ccb的table,后加载的覆盖了前面加载的。这里只是举一个简单的例子,实际应用可能比这个情况复杂得多。
  3. 嵌套CCBFile时,无法对子CCBFile做操作的问题。可能子CCBFile绑定了某个lua对象,有一些函数可以调用,有一些成员变量可以使用。但是全在全局ccb里面,父节点根本不知道子CCBFile对应全局ccb的哪个对象。

解决方案

对于lua这样的脚本语言来说,完全可以在加载ccbi的过程中,动态加载需要的lua文件,进行绑定,我们需要做的只是设定一个绑定规则。然后对CocosBuilder做一些小改动,让publish的时候,同时生成lua文件,那整个开发流程将大大被缩短。

So,为了解决上面的3个问题,我做了两件事

  1. 修改了CCBReader读取的一些方法,提供给lua读取嵌套CCBReader的方法。并且重写了lua读取ccbi的代码。
  2. 修改了CocosBuilder的源代码,让它在publish的时候,直接生成对应的lua文件。

准备工作

1.下载以下cpp文件,覆盖掉cocos2dx的相应文件,然后重新编译Runtime

PS: 值得注意的是,我修改的是cocos2dx 3.2,不确定覆盖更高或者更低的版本会不会出问题,如果不是3.2的用户,可以查看这个commit,自行修改相应代码。

2.下载以下lua文件,放到lua项目的src对应目录下

3.获取修改过的CocosBuilder

有2个方法

  1. 从Github clone源代码自己编译:https://github.com/jennal/CocosBuilder
  2. 下载我编译好的版本:https://github.com/Jennal/CocosBuilder/blob/master/Release/CocosBuilder.app.tar.gz?raw=true

PS: 值得注意的是,由于我们的项目使用的默认设计尺寸是1200x800,所以代码中的缩放比例都是针对这个分辨率的。修改也不难,在这里:CocosBuilder/ccBuilder/ResolutionSetting.m,把顶部的两个define改成你自己要的设计尺寸就行了。

#define DESIGN_WIDTH 1200
#define DESIGN_HEIGHT 800

使用方法

改完Cocos2dx的框架代码,下载了lua代码,有了Cocosbuilder的修改版,准备完成,正式进入使用阶段。

1.ccb文件制作

ccb文件的制作基本没有什么特别,只是有几个点需要注意:

  1. Publish Settings里面多了个选项,记得勾起来。新建项目,默认是勾的。
    Publish Settings
  2. 文件名与该文件的Controller名,必须保持一致,这是为了简化接口,也为了让代码与设计统一
    文件名与该文件的Controller名,必须保持一致

2.代码绑定

生成的lua文件已经包含了一些注释,几乎不用有太多额外的代码,就可以很容易地绑定ccbi文件与lua类。

下面讲解几个需要注意的地方

  1. CCBLoader:setRootPath的两个参数

--[[
第一个参数表示绑定lua文件的package前缀
第二个参数表示ccb文件的路径
]]
CCBLoader:setRootPath("UI.ccb", "ccb/")

  1. 生成的lua文件的ctor相当于cpp中使用ccbi的onNodeLoaded,换句话说,代码执行到这里的时候,这个节点的子节点以及绑定都应该已经完成,可以放心使用了。可以在这里做一些初始化数据的工作。
  2. 创建ccbi节点的方法

local mainLayer = require("UI.ccb.MainLayer"):new()

示例下载

https://github.com/Jennal/CocosBuilder/blob/master/Sample/CCBSample-lua-binding.tar.gz?raw=true

感谢

虽然LuaProxy的方案并不完美,但也为我的改进铺平的道路,在这里感谢LuaProxy的作者shawnclovie

]]>
https://jennal.com/2015/01/30/cocosbuilder-lua/feed/ 5
lua的面向对象库 https://jennal.com/2015/01/04/lua-oop/ https://jennal.com/2015/01/04/lua-oop/#respond Sun, 04 Jan 2015 09:00:52 +0000 http://jennal.com/?p=799 接上一篇文章cocos2d-x中lua-binding的面向对象开发的研究,终于有时间整理一下我自己写的面向对象方案。
先上github地址:https://github.com/Jennal/LuaUtils/blob/master/src/Oop.lua

为什么要写这个库?

我们一直使用cocos2d-x来作为底层框架进行开发。之前用的是cpp,最近转到lua,所以难免保留一些cpp的习惯,包括饱受诟病的多继承。我不想讨论多继承有多好,或者有多糟。我认为,作为库来说,应该尽量提供符合大家习惯的机制,至于是否使用,如何使用,取决于用户。

我们知道lua在语言级别并没有支持类,而是提供了更灵活的metatable,我们可以利用metatable来实现类似cpp的类。cocos2d-x的lua绑定提供了类的支持,支持创建lua级别的类,也支持从cpp继承,但是对于面向对象机制来说,只提供继承,远远不够。

我想要的面向对象机制

  • 多层级继承
    • 从lua继承
    • 从lua继承后的lua继承
    • 从cpp继承
    • 从cpp继承后的lua继承
  • 单继承
  • 多继承
    • 调用不同父类的构造函数
    • 调用不同父类的同名函数
    • 多继承不允许从2个或者以上的cpp继承
  • 接口继承
    • 接口用来约束必须实现的成员函数
  • 判断是否是某个类的实例
    • 包括是否是某个父类的子类的实例

如何使用我的库

单继承

从lua创建对象

local Oop = require("Oop")
-- 从lua创建对象
local A = Oop.class("A")
-- 构造函数
function A:ctor(arg)
    self.arg = arg
    print("create A")
end
-- new的参数将传递到ctor中
local a = A:new("hello") -- create A
print("a.arg", a.arg) -- a.arg  hello
-- 从lua创建的类继承
local B = Oop.class("B", A)
function B:ctor(arg, brg)
    -- 调用父类的构造函数
    B.super.A.ctor(self, arg)
    self.brg = brg
    print("create B")
end
local b = B:new("hello", "world") -- create A \n create B
print("b.arg", "b.brg", b.arg, b.brg) -- b.arg  b.brg   hello   world

从cpp创建对象

-- 从cpp创建对象
local SpriteEx = Oop.class("SpriteEx", function(...)
    return cc.Sprite:create(...)
end)
-- 构造函数
function SpriteEx:ctor(...)
    self.arg = {...}
    print("create SpriteEx")
end
-- new的参数将传递到上面的匿名function中,以及ctor中
local sprite = SpriteEx:new("hello.png") -- create SpriteEx
print("sprite.arg", unpack(sprite.arg)) -- sprite.arg   hello.png
-- 从cpp创建的类继承
local SpecialSprite = Oop.class("SpecialSprite", SpriteEx)
function SpecialSprite:ctor(...)
    -- 调用父类的构造函数
    B.super.A.ctor(self, ...)
    print("create SpecialSprite")
end
local ss = SpecialSprite:new("hello.png") -- create SpriteEx \n create SpecialSprite
print("ss.arg", unpack(ss.arg)) -- ss.arg   hello.png

多继承

纯lua多继承

local A = Oop.class("A")
function A:ctor(arg)
    self.arg = arg
    print("create A")
end
function A:show()
    print("A show", self.arg)
end
local B = Oop.class("B")
function B:ctor(arg)
    self.arg = arg
    print("create B")
end
function B:show()
    print("B show", self.arg)
end
local C = Oop.class("C", A, B)
function C:ctor(arg)
    -- 调用不同父类的构造函数
    C.super.A.ctor(self, arg)
    C.super.B.ctor(self, arg)
end
function C:showA()
    -- 调用不同父类的同名函数
    C.super.A.show(self)
end
function C:showB()
    -- 调用不同父类的同名函数
    C.super.B.show(self)
end
local c = C:new("Hello") -- create A \n create B
c:showA() -- A show Hello
c:showB() -- B show Hello

lua与cpp的多继承

local Oop = require("init")
local A = Oop.class("A")
function A:ctor(arg)
    self.arg = arg
    print("create A")
end
function A:show()
    print("A show", self.arg)
end
local B = Oop.class("B", function(...)
    return cc.Sprite:create(...)
end)
function B:ctor(arg)
    self.arg = arg
    print("create B")
end
function B:show()
    print("B show", self.arg)
end
local C = Oop.class("C", A, B)
function C:ctor(arg)
    C.super.A.ctor(self, arg)
    C.super.B.ctor(self)
end
function C:showA()
    C.super.A.show(self)
end
function C:showB()
    C.super.B.show(self)
end
local c = C:new("Hello.png") -- create A \n create B
c:showA() -- A show Hello.png
c:showB() -- B show Hello.png
-- layer:addChild(c)

接口

-- 定义一个接口
local IA = Oop.interface("IA", {
    {"func1", "(param1, param2)", "description"}, -- 成员函数1
    {"func2", ""}, -- 成员函数2
},{
    {"member", ""} -- 成员变量
})
IA.func1() -- 抛出异常: func1(param1, param2) is not implemented
local test = IA.member -- 抛出异常: member is not defined
local IAImpl = Oop.class("IAImpl", Oop.Obj, IA)
function IAImpl:func2()
    return true
end
IAImpl.member = 1
-- 实现接口
local o = IAImpl:new()
o:func1() -- 抛出异常: func1(param1, param2) is not implemented
print(o:func2()) -- true
print(o.member) -- 1

判断是否某个类或接口的实例

Oop.isClass(obj, "A") -- false
Oop.checkClass(obj, "A") -- 抛出异常:param should be A type
Oop.checkClass(obj, "A", 2) -- 抛出异常:param2 should be A type

]]>
https://jennal.com/2015/01/04/lua-oop/feed/ 0
在MacOSX下用cmake编译cocos2dx的笔记 https://jennal.com/2014/12/06/cocos2dx-cmake-note/ https://jennal.com/2014/12/06/cocos2dx-cmake-note/#respond Sat, 06 Dec 2014 09:57:08 +0000 http://jennal.com/?p=796 最近项目想用Qt来做cocos2d-x的跨平台编辑器。由于对Qt和OpenGL都是新手,所以想用相对低成本的方法来做,参考了不少项目。发现好多人有用Qt来做cocos2d-x的编辑器的想法,包括cocos2d-x官方,但不知道什么原因,几乎所有的项目都中途流产了。大部分项目使用的是相对较旧的cocos2d-x版本,有些甚至无法编译成功。不过这些项目也给了我不少帮助,参考了很多有价值的信息。

最后,我决定自己做一个QtPort,选用CMake来做项目管理,因为新版的cocos2d-x已经带了CMakeLists.txt,如果直接用Qt的pro/pri/prf等项目文件,那工程量就太大了。

工具

otool

这个工具帮了很大忙,因为使用CMake,所有需要链接的库都需要自己设置。而我不知道需要哪些库,利用otool -L xxx来查看用XCode编译出来的xxx使用了哪些库,也让我顺利把所需的库都加进来。

安装组件

首先要用到的就是HomeBrew了。安装完HomeBrew,就可以用它安装各种东西了。

> brew update
> brew install glew jpeg webp libtiff freetype libwebsockets glew

这里教大家一个小技巧,当编译提示

library not found for -lfreetype

这里的-l后面就是缺失的库的名字,可以把这个名字直接放到brew install后面进行安装。
如果名字不对的话,也可以使用brew search freetype进行搜索。

问题

  • Spine库的CCSkeleton.cpp文件会编译不过,我们也用不到Spine库,所以暂时不编译它。
  • Audio相关的库编译不过,我在CMakeLists里加了个AUDIO_BUILDoption的选项,也暂时关闭它。
  • 编译lua时遇到了比较大的问题
    • 首先,因为我们禁用了Spine和Audio的编译,但是cocos2dx的tolua代码中,并没有根据这个条件编译进行判断,是否包含这些文件。为了防止使用这些文件出问题,我加了#ifdef来判断,当定义了不要Spine或者Audio时,把相关的lua函数都注册成空函数。
    • 然后是luasocket的问题,cocos2dx官方根本就没有写luasocket的CMakeLists.txt,不过写起来还算顺利,唯一需要注意的是luasocket的wsocket.c和usocket.c,是需要分平台编译的,wsocket.c是针对Win平台写的,usocket.c是针对unix-like系统写的。

关于CMakeLists的代码

好像没有特别需要说明的,基本就是CMake的规则,唯一值得一提的是,MacOSX下面的编译,记得这么做

# add the executable
add_executable(${APP_NAME}
  MACOSX_BUNDLE
  WIN32
  ${SAMPLE_SRC}
)

这样生成的目录结构才会是这样的

AppName.app/
AppName.app/Contents
AppName.app/Contents/Info.plist
AppName.app/Contents/MacOS
AppName.app/Contents/MacOS/AppExecutable

最后

最后给出我的项目地址:https://github.com/Jennal/cocos2dx-3.2-qt

]]>
https://jennal.com/2014/12/06/cocos2dx-cmake-note/feed/ 0
[转]在MacOSX下制作icns图标 https://jennal.com/2014/12/04/make-icns-in-macosx/ https://jennal.com/2014/12/04/make-icns-in-macosx/#respond Thu, 04 Dec 2014 03:09:09 +0000 http://jennal.com/?p=788 以下内容转载自:http://www.cocoachina.com/bbs/read.php?tid=113763

步骤1

在任何一个目录下面创建一个文件夹,命名为 [name].iconset 例如 icon.iconset

步骤2

在该文件里面放入以下图片文件,并核对尺寸是否正确

图片名称 尺寸
[email protected] 1024×1024
icon_512x512.png 512×512
[email protected] 512×512
icon_256x256.png 256×256
[email protected] 256×256
icon_128x128.png 128×128
[email protected] 64×64
icon_32x32.png 32×32
[email protected] 32×32
icon_16x16.png 16×16

步骤3

打开终端,在里面输入以下命令

iconutil –c icns **/[name].iconset

例如:

iconutil –c icns /Users/zhudongyong/Desktop/icon.iconset

注:可以先输入iconutil –c icns(后面需带一个空格),再把步骤1所创建的文件夹拖到终端,则会自动把该文件夹的路径添加到刚输入的命令后面。

步骤4

在步骤3后,系统会自动生成[name].icns(icon.icns)文件,将该icns文件导入工程,并设置为Icon即可。

Apple官方原文由此进

]]>
https://jennal.com/2014/12/04/make-icns-in-macosx/feed/ 0
解决cocos2dx-lua绑定print不能打印指针的问题 https://jennal.com/2014/10/29/cocos2dx-lua-print-fix/ https://jennal.com/2014/10/29/cocos2dx-lua-print-fix/#respond Wed, 29 Oct 2014 08:39:07 +0000 http://jennal.com/?p=785 cocos2d-x的lua绑定,把原生的print函数给改掉了,就为了加个cocos2d: [LUA-print]的前缀,实在让人无语。临时工为了方便,直接去掉了table、user data可以显示指针地址的功能,所以我只好去改源代码了。

有2种改法,先说复杂的

找到这个文件

cocos2d-x/cocos/scripting/lua-bindings/manual/CCLuaStack.cpp

修改这个函数

lua_print

直接替换整个函数的代码

int lua_print(lua_State * luastate)
{
    int nargs = lua_gettop(luastate);
    std::string t;
    for (int i=1; i <= nargs; i++)
    {
        const char * str = lua_tostring(luastate, i);
        if (str)
        {
            /* number, string */
            t += lua_tostring(luastate, i);
        }
        else if (lua_isnone(luastate, i))
            t += "none";
        else if (lua_isnil(luastate, i))
            t += "nil";
        else if (lua_isboolean(luastate, i))
        {
            if (lua_toboolean(luastate, i) != 0)
                t += "true";
            else
                t += "false";
        }else{
            /* table, userdata, lightuserdata, thread */
            t += lua_typename(luastate, lua_type(luastate, i));
            char pointer[16];
            memset(pointer, 0, 16);
            sprintf(pointer, "%p", lua_topointer(luastate, i));
            t += ": ";
            t += pointer;
        }
        if (i!=nargs)
            t += "\t";
    }
    CCLOG("[LUA-print] %s", t.c_str());
    return 0;
}

简单的办法

还是这个文件

cocos2d-x/cocos/scripting/lua-bindings/manual/CCLuaStack.cpp

找到这段代码

// Register our version of the global "print" function
    const luaL_reg global_functions [] = {
        {"print", lua_print},
        {NULL, NULL}
    };

直接注释掉,这样就可以用lua原生的print函数了。如果还是想要那个前缀,很简单,只需要在lua文件里面写就可以了,具体就不用多说了。

]]>
https://jennal.com/2014/10/29/cocos2dx-lua-print-fix/feed/ 0
cocos2d-x中lua-binding的面向对象开发的研究 https://jennal.com/2014/10/18/cocos2dx-lua-oop/ https://jennal.com/2014/10/18/cocos2dx-lua-oop/#comments Sat, 18 Oct 2014 14:06:59 +0000 http://jennal.com/?p=778 起因

网路上已经有很多关于lua面向对象开发的文章,为什么我要自己写一篇呢?

一切都是因cocos2d-x 3.2的lua绑定而起。我们的新项目打算用lua进行开发,一直在跟进quick-x项目,从独立、第三方,到现在的被触控收编。cocos2d-x 3.2是最新的稳定版,把quick-x的很多好东西吸纳进来。与其说吸纳,不如说是直接把源代码拿过来了。所以,现在cocos2d-x 3.2直接就支持lua的面向对象开发,提供了原来只有quick-x才有的class等方法来方便的创建对象以及做继承。

既然cocos2d-x官方都认为quick-x的东西很好,我也不得不研究一下这套针对lua开发的创建类和继承的方法。不研究不要紧,一研究吓一跳。备受关注的quick-x,把自己宣传的那么好,那么便捷(我刚开始研究quick-x的时候,用它写过一个项目,提供的接口确实很便捷,大大提高了开发效率,但是没有研究实现细节),没想到最最底层,最最基础的部分,创建类、类继承的方案却如此业余,简直是一个刚学lua两三天的“高手”写出来的。说刚学两三天,是因为,从这个方案可以看出,完全没有理解lua语言的真谛,没有做到thinking in lua,而“高手”并不是贬义,一般人真的做不出这样的设计,写不出这样的代码。

分析

当然,也不是说这份代码一无是处(下个章节会详细分析),我这里先说说这份代码的优点和缺陷,仅代表个人意见,希望能引起共鸣。

优点

  1. 分解了创建实例(cls.__create)和构造函数的方法(instance:ctor),这样做的好处很明显,让混沌的思路清晰起来,并且“支持”从function继承,这也是支持从cpp对象继承的基础。
  2. 创造了类与类的实例之间的区别
    • 类:cls.class == nil
    • 类的实例:instance.class ~= nil
  3. 支持从cpp、lua的table继承,也支持创造新的类
  4. 支持默认构造函数
    • 当创建新的类时,有一个什么都不做的构造函数
    • 当lua的table继承时,可以调用父类的构造函数

缺陷

  1. 不支持多继承,这对于从cpp开发转过来的开发人员来说,可能是最大的问题,因为lua的特性其实是可以支持多继承的
  2. 没有充分利用lua的特性(下面的review详细展开讲)
    • 从lua的table继承时,不需要做clone,clone会失去很多好处,并且带来额外的内存开销
  3. 构造函数在继承过程中存在设计缺陷(下面的review会展开讲)

小结

所以总体上来看,还是优点多于缺点的。但是对于用惯了多继承(即使是不支持多继承的语言,比如Java、C#也是支持多接口实现的)的我来说,这点实在不可忍,所以不得不自己写了一套继承机制,为了避免这篇文章太长,将在下一篇文章中公开。

代码review

下面是重头戏了,让我们来看看class函数的代码。

--Create an class.
function class(classname, super)
    -- Part 1 --
    local superType = type(super)
    local cls
    if superType ~= "function" and superType ~= "table" then
        superType = nil
        super = nil
    end
    -- End of Part 1 --
    if superType == "function" or (super and super.__ctype == 1) then
        -- Part 2 --
        -- inherited from native C++ Object
        cls = {}
        if superType == "table" then
            -- copy fields from super
            for k,v in pairs(super) do cls[k] = v end
            cls.__create = super.__create
            cls.super    = super
        else
            cls.__create = super
        end
        cls.ctor    = function() end
        cls.__cname = classname
        cls.__ctype = 1
        function cls.new(...)
            local instance = cls.__create(...)
            -- copy fields from class to native object
            for k,v in pairs(cls) do instance[k] = v end
            instance.class = cls
            instance:ctor(...)
            return instance
        end
        -- End of Part 2 --
    else
        -- Part 3 --
        -- inherited from Lua Object
        if super then
            cls = clone(super)
            cls.super = super
        else
            cls = {ctor = function() end}
        end
        cls.__cname = classname
        cls.__ctype = 2 -- lua
        cls.__index = cls
        function cls.new(...)
            local instance = setmetatable({}, cls)
            instance.class = cls
            instance:ctor(...)
            return instance
        end
        -- End of Part 3 --
    end
    return cls
end

为了让没有使用过这个函数的读者也能读懂,先说一下函数的用法吧

--- Sample1: 创建新的类
-- 创建一个名为ClassA的类
local ClassA = class("ClassA")
-- 创建ClassA类的实例
local a = ClassA.new()
-- ClassA的构造函数
function ClassA:ctor()
    self.name = "a"
end
local aa = ClassA.new()
print(aa.name) -- 输出:a
--- Sample2: 从lua的table继承
-- 创建一个名为ClassB的类,ClassB继承于ClassA
local ClassB = class("ClassB", ClassA)
-- 创建ClassB类的实例
local b = ClassB.new()
--- Sample3: 从cpp对象(在lua中是userdata)继承
-- 创建一个名为ClassC的类,ClassC继承于cc.Scene
local ClassC = class("ClassC", function()
    return cc.Scene:create()
end)
-- 创建ClassC类的实例
local c = ClassC.new()

总体上看,这个函数可以分为3个部分(我已经在代码中用注释标明)。

  1. 局部变量声明以及参数类型检查
  2. 从cpp类继承
  3. 从lua的table继承

局部变量声明以及参数类型检查

local superType = type(super) -- 父类的类型,用于后面决定要进入哪个分支
local cls -- 这将是最后的返回值
-- 父类的类型必须是function或者table,如果都不是,就当做super传的是nil,也就是说当做创建新类来处理
if superType ~= "function" and superType ~= "table" then
    superType = nil
    super = nil
end

从cpp类继承

-- inherited from native C++ Object
cls = {} -- 初始化cls
-- 父类是从cpp的类继承的
if superType == "table" then
    -- 从父类拷贝所有的元素,我认为这里是有问题的,详见下面的问题1
    for k,v in pairs(super) do cls[k] = v end
    cls.__create = super.__create -- 我想这个只是用来提醒自己,其实这个赋值已经包含在上面的for循环中了
    cls.super    = super
else
    -- 传进来的super是function,__create用来创建类的实例
    cls.__create = super
end
cls.ctor    = function() end  -- 默认构造函数,我认为这里有2个问题的,详见下面的问题2和问题3
cls.__cname = classname  -- 类名
cls.__ctype = 1  -- 类的类型,代表是从cpp的类(userdata)继承的
-- 类的new方法,同问题3
function cls.new(...)
    local instance = cls.__create(...)  -- 创建类的实例
    -- 拷贝所有的元素到实例中,我认为这里也是有问题的,详见下面的问题4
    for k,v in pairs(cls) do instance[k] = v end
    instance.class = cls  -- 对实例的class进行赋值,可以从实例找到cls
    instance:ctor(...)  -- 调用构造函数
    return instance
end

问题1:从父类做深度拷贝

这里的super已经是拷贝过cpp类(userdata)的所有元素,已经是个lua的table了,我认为这里可以完全当做从lua的table继承来处理,而没必要再做一次深度拷贝。

问题2:默认的空构造函数

先理一下代码走到这里的前提

  • super是function
  • 或者super是从cpp类继承的table

你想到了什么?对,如果super是从cpp类继承的table,那么这里的默认构造函数就屏蔽了父类的构造函数。当然,在构造函数ctor里面有办法调到父类的构造函数的,可以这么做self.class.super.ctor(self)。但是回过头来看第一个前提,super是function的时候,super是没有被赋值到self.class.super的,所以现在要调用父类的构造函数的话,莫非还要判断一下?

问题3:不能被重复利用的代码

由于lua的随意性,很容易同一份代码被“写了多次”,又由于lua的函数是第一类对象,也就是说函数是可以被动态创建的。那么所有代码中,被我标记了问题3的地方,这些地方的函数,虽然看起来就一份代码,但是被创建出来以后却全是不同的实例。试想一下,每一个新创建的类型(不是类的实例),都包含了一个冗余的代码块,得有多大的代价?(实际开销可能并不大,但总觉得是个浪费。)

问题4:创建实例时的深度拷贝

这是在干嘛呢?拷贝那么多遍干嘛?都已经拷贝一遍到cls里面了,再拷贝一遍到instance干嘛呢?完全没有thinking in lua嘛!这里只需要setmetatable就可以了呀。

从lua的table继承

-- inherited from Lua Object
if super then
    cls = clone(super)  -- 从父类做深度拷贝,又来了,问题5
    cls.super = super  -- 标记父类
else
    cls = {ctor = function() end}  -- 父类为空时,创建默认构造函数,同上面的问题3
end
cls.__cname = classname  -- 保存类名
cls.__ctype = 2 -- lua  -- 标记这是个lua继承来的类
cls.__index = cls  -- 为setmetatable做准备
-- 类的new方法,同上面的问题3
function cls.new(...)
    local instance = setmetatable({}, cls)  -- “正统的”lua类继承来了
    instance.class = cls  -- 对实例的class进行赋值,可以从实例找到cls
    instance:ctor(...)  -- 调用构造函数
    return instance
end

问题5:从父类做深度拷贝

刚刚从userdata做深度还是可以理解的,但是现在是从lua的table做继承呀,放着setmetatable不用,又clone干嘛呀?

总结

很多观点可能会有人认为偏颇,因为大多是深度拷贝的问题,而深度拷贝的时候,对于table、function、userdata等数据来说,也只是拷贝一个引用,并没有太大的代价。但我始终固执地认为,既然要用lua,就应该用lua的思维来解决问题,特别是底层的部分,而且底层的部分,对于内存和执行效率的考虑应该更深入,更全面一些。

不过至少大家可以达成共识,最大的问题就是问题2。

最后,放出我自己写的Oop库来解决上面的问题,并且有更多惊喜等着你去发现。

]]>
https://jennal.com/2014/10/18/cocos2dx-lua-oop/feed/ 8