Visual Studio Code 视频支持折腾记

由于项目需要,打算使用Markdown作为内容制作的格式。而项目中有不少内容是需要展示音频和视频的地方。也就是说需要让Markdown支持音频和视频格式。

项目程序员的非Vim党们使用的都是Visual Studio Code(后面简称Code)。所以自然想选择Code作为Markdown编辑器了。

所以问题归结为,如何让VSCode预览的时候把Markdown里面的影视频链接变成video和audio标签。

通过一番搜索,发现Code内部Markdown预览使用的是markdown-it,并且提供了扩展markdown-it的接口。
同时markdown-it又现成的markdown-it-html5-embed插件。所以应该是写一个小插件就能了事的。

第一回:Code插件尝试

写个简单的Code插件,为markdown-it加载跟多的插件,只要几行代码:

function activate(context) {
    return {
        extendMarkdownIt(md) {
            return md
                .use(require("markdown-it-html5-embed"), h5embedOptions);
        }
    }
}

挺简单吧,不过如果这就是结局,那也太简单了。

发布代码,测试。

什么灰色的影视频按钮?什么鬼?

继续搜索,Code和音视频支持,结果发现:Code居然在发布的时候移除了全部ffmpeg功能,而且微软不打算支持在Code中使用音视频。

思考人生……

思考人生……

Code不是开源的吗?我自己编译不就好了。

第二回:自己编译Code

说干就干,先克隆代码: https://github.com/microsoft/vscode.git

然后编译,本地运行,看起来都OK。就是运行的时候似乎有些小问题。

再试试打包成 App,结果,正如多数大型JavaScript项目打包一样,他崩溃了。

我只想要一个简单功能,即便能打包成功,每次编译Code也是太麻烦了吧。

等等,我应该不是第一个想吃螃蟹的人,先前一定有人想过这么干。Google一下,还真有。

第三回:VSCodium

是的VSCodium项目就是提供社区编译版本的Code。下载试试,音频视频都能播放。太好了,看来可以结案了。

过了半天,突然想给内容编辑同事展示一下VSCodium里面编辑带音视频的Markdown的情况。先在自己电脑上试试,诶,有点不对,视频这么又灰了。不对是MP4灰了,Webm格式还是可以的。

那么我们继续安装换编辑器的思路,试试Atom把,毕竟以前还用过Atom。

第四回:Atom

Atom里面用Markdown内嵌HTML标签的方式先试试。不错,MP4,MP3格式都可以啊。

当然还是有些不甘心,毕竟现在Atom维护和流畅都比不上Code。尝试把Atom的FFmpeg库拷贝到VSCodium里面,结果预览直接黑屏。放弃,继续验证Atom的思路走下去。

那么就是要让Markdown预览支持把音视频链接换成audio、video标签。发现比较流行的Markdown Preview Enhanced。但是问题又来了,这个插件没有直接使用markdown-it,而是使用了作者自己的mume模块。

去看看mume的代码,但 package.json 里面并没有依赖markdown-it,甚至根本没有第三方markdown模块的依赖。

只好去Markdown Preview Enhanced项目提功能需求。看了看作者还是深圳的,萌生了想付费让作者帮忙支持这个功能的想。打开邮箱,写邮件……

等等,mume应该不会自己写markdown解析,再仔细找找。果然,有个依赖目录,里面真有markdown-it及几个插件的源代码。

那么问题又简单了,赶紧为mume写个PR。提过去后,过了一段时间,收到作者回复,说可以接受PR,需要做些小的修改啥的。受到鼓舞,我当即表示说现在的代码配置的太死,要添加几个配置项,然后顺手把Markdown Preview Enhanced的PR也写了。

带着本地版本的插件,给内容同事演示了一把,基本上满意。

好的,接下来就等作者合并发版了。

第五回:峰回路转

虽然问题基本上解决了,但是Atom毕竟现在插件质量不如Code了。而且要同时安装Code和Atom有点难受。然而压死骆驼的最后一根稻草是Atom的Markdown高亮插件不如Code的。

还是Debug一下为什么不能播放音视频,是在chromium内核层面还是只是剔除了相关的FFmpeg实现?
通过JavaScript接口发现,chromium还是声称能支持MP4、MP3的。

那么应该是可以替换ffmpeg库的。之前替换VSCodium失败应该是ffmpeg版本不一致导致的。

再次Google,发现有个项目专门为NWjs编译ffmpeg。NWjs和Electron(Code和Atom的运行平台)原理差不多。同时记得之前编译Code的时候,在网上下载的是ffmpeg 3.8 的版本,于是下载一个 3.8.1版本的预编译的ffmpeg。替换一下,居然OK了。居然就这么样OK了。

所以最后的办法还是回到的Code,替换ffmpeg库的方式。写个 install.sh 脚本:

#!/usr/bin/env bash


curl -sL -o libffmpeg.zip https://github.com/iteufel/nwjs-ffmpeg-prebuilt/releases/download/0.38.1/0.38.1-osx-x64.zip
unzip libffmpeg.zip
mv ./libffmpeg.dylib "/Applications/Visual Studio Code.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Libraries/libffmpeg.dylib"

当然,这里还可以通过短连接等方式让每次下载的脚本是最新的ffmpeg。具体方法这里就不展开了。

这里我吧他放到这个gist这样就可以通过 curl pipe bash的方式安装。

curl -s https://gist.githubusercontent.com/xpol/483ec9967d0a3d14791374947ecf0ec8/raw/4083e7563f48b9de57503932fce6c2512f70f5c8/install.sh | bash

最后再写个文档,教非编辑同事如何打开终端运行上面这段代码。

完美!(希望这个不是很快倒下的flag

后记

曲折的过程为最终解决方案积累信息。

不去编译一下Code,可能不知道要什么版本的ffmpeg。不先给Code写个插件,那么后面替换ffmpeg的方案可能也要曲折一些。

当收集到的信息足够且充分连接的时候,得出理想的方案只是水到渠成而已。

测试驱动游戏开发:一个例子

什么是测试驱动开发(Test-driven development)?测试驱动开发在开发每一个功能时按照下面的步骤:

  1. 编写单元测试
  2. 运行测试,测试失败
  3. 编写代码
  4. 使测试通过
  5. 重构改进代码

测试驱动开发适合游戏开发吗?

良好设计的游戏逻辑,适合测试驱动开发;界面啊、特效啊、交互啊……这些就不太容易进行测试驱动开发。

下面我们举一个例子来看一下如何对游戏逻辑进行测试驱动开发。

例子

背景

某种升级游戏中,为玩家和 AI 提供跟牌的必选牌和可选牌的组合。代码采用 Lua 编写,测试使用 busted

需求分析

对问题进行分类,从简单的入手,分析如下:

## 头家出 n (n=1,2,4,6,8...) 张牌,跟牌的时候根据当前玩家手牌:
  - 1. 同花色无牌;则无强制出牌,可跟手牌中任意 n 张牌的组合。
  - 2. 同花色有牌但是牌不够或者刚好够:
  - 2.1. 同花色所有手牌必出
  - 2.2. 如果不够,用任意其他手牌补足 n 张

这里只列出了两种最简单的情况,实际的列表要长的多,列表嵌套的层级也更深。

需要注意的是,同等级的点之间一定要完整覆盖问题,而且不从叠。

添加单元测试

基于 busted 编写第一个需求,缺牌情况下的单元测试如下:

describe(':findFollows()', function()
  describe('1. when led suit is void in hand.', function()
    it('allows play any cards in hand', function()
      local hand = Hand('♥9', {'♥A','♥A','♠7','♣A','♣K','☆','★'})
      local mandatory, choices = hand:findFollows('♦', 1)
      assert.are.same({}, mandatory) -- no mandatory cards

    end)
  end)
end)

describe 中采用和需求相同的编号来表示这段代码对应的需求,下面实现代码注释也是这样标识的。

这个时候运行单元测试,将出现测试失败。

实现

接下来完成第一中情况下的代码实现:

function Hand:findFollows(ledsuit, n)
  local totalCardsInLeadSuit = ...
  local cardsInLedSuit = ...

  -- 1.

  if totalCardsInLeadSuit == 0 then
    return {}, combineOf(self:all(), n)
  end
end

这里省略了部分实现细节,只列出了代码骨架。在实现过程中不断运行测试,直到通过就表示完成改功能了。

重构

之后可以进行代码进行重构和改进。在这个过程中,有可能发现一些额外的测试需要添加。

第二个功能的单元测试

然后开始下一个需求的单元测试编写:

  describe('2. have not enough led suit cards in hand', function()
    local hand
    before_each(function()
      hand = Hand('♥9', {'♦A','♦A','♠7','♣A','♣K','☆','★'})
    end)
    it('2.1. all cards in led suit are mandatory', function()
      local mandatory, _ = hand:findFollows('♦', 4)
      assert.are.same({'♦A','♦A'}, mandatory)
    end)
    it('2.2. rest use all cards combinations', function()
      local _, choices = hand:findFollows('♦', 4)
      assert.are.same({low={'♠7','♣A'}, score={'♣K', '♠7'}}, choices)
    end)
  end)

第二个功能的实现

然后对应的实现:

function Hand:findFollows(ledsuit, n)
  -- 1.

  -- 2.

  if totalCardsInLeadSuit <= n then
    local nl = n-totalCardsInLeadSuit
    local others = {}
    if nl > 0 then
      others = self:cardsExcept(ledsuit)
    end
    return cardsInLedSuit, combineOf(others, nl)
  end
end

重构

再次,可能才实现中的代码有些不优雅的地方,重构它。

循环

按照上面的循环进行更多功能的开发:

到现在你大概理解单元测试和游戏开发如何结合了。

结束语

测试驱动游戏开发要点:

  1. 测试从需求作为出发点,这样能保证实现的功能是需求想要的;
  2. 按照 写测试、测试失败、写功能代码、测试通过、重构 的循环来进行开发;
  3. 用和需求相同的编号来标识功能点,方便功能跟踪。

其实这样已经有点行为驱动开发(Behavior-driven Development)的味道了,就是用单元测试来表达用户需求。

Fixes SourceTree Templates Not Found Warning

There is a warning in SourceTree when working with git. It confuse me for a long time:

warning: templates not found C:\Program Files\Git\share\git-core\templates

This warning may caused by SourceTree which configured the templatedir to C:\Program Files\Git\share\git-core\templates and is not correct for 64 bit git on Windows. For 64 bit git it default install its templates to C:\Program Files\Git\share\git-core\templates.

To fix this warnings, either edit .gitconfig in the user directory (%USERPROFILE%/.gitconfig).

[init]
templatedir = C:\\Program Files\\Git\\mingw64\\share\\git-core\\templates

Or via command line:

git config --global --replace-all init.templatedir = "C:\Program Files\Git\mingw64\share\git-core\templates"

Note: You may have git for Windows installed at other location, change the path set above.

UPDATE:

For mac user if your are using system git, the template path is /Library/Developer/CommandLineTools/usr/share/git-core/templates.

用 Atom 打造您的 cocos2d-x Lua 开发环境

用 cocos2d-x 一年有余了,当初的初学用户已经成为了 cocos2d-x 代码的贡献者。确实 cocos2d-x 有很多问题,需要用户不仅仅作为用户;还要提出解决方案、提交 pull request 来为 cocos2d-x 作出贡献才能让 cocos2d-x 越来越好。

相应的,作为 cocos2d-x Lua 用户,在编辑器周边,在这一年多的时光里面,我编写了三个 Atom 插件:

这里就介绍一下这几个插件的安装和使用。不过首先我们先安装 Atom

官网下载 墙内下载

linter-luacheck 进行错误检查

Lua 是一门脚本语言,有许多坑。它的变量可以不用声明直接使用,这难免带来错误。比如把局部变量打错一个字幕,他不会报错。这就需要有语法检查工具来帮助了。这里介绍如何在 Atom 中安装 [luachcek][] 的插件来进行语法检查:

  1. 安装 Lua:*nix 可以从源码编译,也可以用系统的包管理器,Mac OS X 可以用 brew。Windows 用户有 Lua for Windows (但是我还没有测试过是否可以安装 LuaRocks)。
  2. 安装 LuaRocks
  3. 安装 luacheck 命令行工具: luarocks install luacheck
  4. 好了,终于可以安装 linter 和 linter-luacheck 了: apm install linter linter-luacheck
  5. 配置,Mac OS X 下可能需要配置一下 luahcek 的路径: 以 Mac OS X 为例: Atom -> Preferences... -> Packages -> linter-luacheck -> Settings -> Executable,然后填写你的 luacheck 的绝对路径。
  6. 高级配置,luacheck 有许多配置选项,可以在项目根目录下建立一个 .luacheckrc 进行详细的配置。

.luacheckrc 示例:

std = 'max'
globals = {'cc', 'ccui', 'ccs', 'dump', 'transition', 'class'}
exclude_files = {'src/cocos/**.lua'}
self = false
files['.luacheckrc'].global = false

以后,打开 Lua 文件的时候就可以看到错误检查的提示了。以后就靠你自己保持每个 Lua 文件零警告零错误了。

build-busted 进行单元测试

「什么?单元测试?」 有人可能惊讶的问,「游戏客户端怎么单元测试啊?不都是安装到设备上由测试人员进行测试吗?」

确实可以,不过我说的不是 UI 的测试,我说的是逻辑层的单元测试。把游戏的逻辑、显示和控制分离(比如 MVC)后,就可以对逻辑进行单元测试了。

Lua 单元测试框架选择并不是太多,但是我们任然有非常优雅的 busted 框架,具体用法看官方文档就好了,这里讲下怎么安装环境。

  1. 安装 Lua 和 LuaRocks,前面已经介绍过了,你应该已经会了。
  2. 安装 busted:luarocks install busted.
  3. 编写测试,放在你的 cocos2d-x Lua 项目根目录下的 spec 目录下。每个测试文件以 _spec.lua 结尾。
  4. 安装 build-busted: apm install build build-busted
  5. 配置:和前面的 inter-luacheck 类似,Mac OS X 下可能需要在 Atom 的设置里面 busted 命令的绝对路径。
  6. 在项目根目录下创建一个 .busted 文件,这样 build-busted 就知道你是在用 busted 进行测试了。
  7. 运行测试:Cmd Alt T / Ctrl Alt T / F7 显示编译、运行目标列表,选择 busted

运行结果像这样,还可以定位到错误发生的代码:

build-cocos 编译运行发布 cocos2d-x 项目

build-cocos 插件能让你在 Atom 下完成 cocos2d-x C++, Lua 和 Javascript 项目的编译、运行和发布等任务。更重要的是,对于编译和脚本运行错误都能够捕捉和定位到源代码。再也不用在命令行和代码编辑器中间来回切换了。

让我们先来安装:

  1. 按照文档安装 cocos2d-x 环境。
  2. 安装定制版的 cocos 命令:在到项目根目录:git clone https://github.com/xpol/cocos2d-console frameworks/cocos2d-x/tools/cocos2d-console ,这样 build-cocos 会优先使用项目内的的 cocos 命令。
  3. 安装build-cocosapm install build build-cocos
  4. 运行测试:Cmd Alt T / Ctrl Alt T / F7 显示编译、运行目标列表,选择你要的编译或者运行的目标。

C++编译错误:

Lua 脚本运行错误:

好了,以上就是粗略的介绍,其实 linter-luacheck build-bustedbuild-cocos 都还有许多细节的用法,需要你慢慢去发现。有问题也可以在 github 上提。

如何优雅地为 cocos2d-x 添加或修改 Lua 绑定?

cocos2d-x 是目前很流行的开源游戏引擎。由于开源的便利,即便是在以 Lua 为开发的语言的项目里面,大家都会包含 cocos2d-x 的源代码。这为需要的时候修改 cocos2d-x 源代码提供了方便。然而,直接修改 cocos2d-x 源代码带来的坏处是:下次需要更新 cocos2d-x 版本的时候会很麻烦,一不小心就会弄丢自己的改动。更好的做法是可以将修改部分提交 pull request 到 cocos2d-x。如果运气好的话,cocos2d-x 的维护者会很快合并你的修改。当然,如果你的 pull request 半天没有人理,即便理了,很久都没有人合并,那么就很难达到想要的效果了。因此我们需要一个替代的方法。这里我介绍下我在项目里面采用的修改 cocos2d-x Lua 绑定的方法流程。

预备知识

  1. 你需要熟悉 git 和 GitHub
  2. 你需要熟悉如何使 Lua C API 和 cocos2d-x 使用的 tolua++ 系统

仓库管理

  1. 一个项目仓库(那是必须的),其中包一份由 cocos new ... 命令为你拷贝的一份位于 frameworks/cocos2d-x cocos2d-x项目本地源代码(后面简称这份 cocos2d-x 源代码为项目拷贝
  2. 一份 cocos2d-x 官方源代码的仓库(后面我们称这个仓库为官方仓库
  3. 你在 GitHub 上有一份 cocos2d-x 的 fork,将 cocos2d-x 官方仓库地址在 git 里面 remote 名设置为 upstream,将你的 fork 的地址设置为 origin

修补流程

当遇到 cocos2d-x 有 bug 或者缺少你想要的绑定的时候:

  1. 先修改项目拷贝源代码,作出你要的修改,测试通过(不过最好不要在项目代码中提交对 cocos2d-x 下面的修改,原因见下一步,下一步会进一步处理这些修改)
  2. checkout cocos2d-x 官方分支对应你使用版本的分支 (比如你用的是 3.9.0 那就拉取 v3 分支),本地分支名以你的修改命名,比如 add-xx-binding-to-xxx
  3. 将项目拷贝文件夹和官方仓库Beyond Compare 之类的工具进行对比,把在项目拷贝中修改部分复制到官方仓库
  4. 将上一步的更改提交,push 到 origin (即你的 fork 地址)
  5. 登录 GitHub,你的 cocos2d-x fork 项目页面,提及 pull request

隔离修改

书分两头,很可能 cocos2d-x 不能很快合并你的 pull request 甚至拒绝。你需要将你在项目拷贝中的修改移动到 framewroks/runtime-src/Classes 下面,当然建议弄个专门的目录,比如 framewroks/runtime-src/Classes/cocos2d-x-hotfixes。这样可以让你对项目拷贝不作出修改,这样下次需要升级 cocos2d-x 版本的时候基本上可以直接覆盖,(当然,你可能需要合并 ccConfig.h

比如我在项目里面添加了EventDispatcher::addCustomEventListener()的 Lua 绑定,我需要这样做:

实现

首先我在 framewroks/runtime-src/Classes/cocos2d-x-hotfixes/EventDispatcher_addCustomEventListener-hotfixes.cpp中:

  1. 实现 static int cc_EventDispatcher_addCustomEventListener(lua_State* L);
  2. 在末尾添加注入虚拟机的代码:
int EventDispatcher_addCustomEventListener_hotfixes(lua_State* L)
{
    tolua_open(L);
    tolua_module(L, "cc", 0);
    tolua_beginmodule(L, "cc");
        tolua_module(L, "EventDispatcher", 0);
        tolua_beginmodule(L,"EventDispatcher");
            tolua_function(L,"addCustomEventListener",cc_EventDispatcher_addCustomEventListener);
        tolua_endmodule(L);
    tolua_endmodule(L);

    return 0;
}

导出

第二步,然后添加framewroks/runtime-src/Classes/cocos2d-x-hotfixes/EventDispatcher_addCustomEventListener-hotfixes.hpp,内容如下:

#pragma once
#ifndef __EventDispatcher_addCustomEventListener_hotfixes_HPP__
#define __EventDispatcher_addCustomEventListener_hotfixes_HPP__

#if __cplusplus
extern "C" {
#endif

#include "lua.h"

int EventDispatcher_addCustomEventListener_hotfixes(lua_State* L);

#if __cplusplus
}
#endif

#endif

注入

第三步,在 frameworks/runtime-src/Classes/lua_module_register.h 中:

  1. 添加 #include "path/to/EventDispatcher_addCustomEventListener-hotfixes.hpp"
  2. static int lua_module_register(lua_State* L) 尾部添加:
static int lua_module_register(lua_State* L)
{
    //Don't change the module register order unless you know what your are doing
    register_cocosdenshion_module(L);
    register_network_module(L);
    register_cocosbuilder_module(L);
    register_cocostudio_module(L);
    register_ui_moudle(L);
    register_extension_module(L);
    register_spine_module(L);
    register_cocos3d_module(L);
    register_audioengine_module(L);
#if CC_USE_3D_PHYSICS && CC_ENABLE_BULLET_INTEGRATION
    register_physics3d_module(L);
#endif
#if CC_USE_NAVMESH
    register_navmesh_module(L);
#endif
    EventDispatcher_addCustomEventListener_hotfixes(L);
    return 1;
}

收尾

最后,提交到你的项目中。

总结

虽然我个人认为目前 cocos2d-x 处理 issues 和 pull requests 的速度有点打击参与者的积极性。然而在这样的情况下,这个流程能很好的适应 cocos2d-x 目前的情况:

进,能为 cocos2d-x 贡献代码和补丁;退,能将修改隔离,方便项目升级 cocos2d-x 的版本。

如果将来 cocos2d-x 官方合并了你的代码,你可以升级到相应的版本,然后删除上面的这些代码。

用C++实现简单工厂模式

简单工厂是一种简单的设计模式用于通过不同的标识创建不同的具体产品类型。

虽然不属于GoF的23种设计模式,但是它还是很有用处的。

实现

简单工厂最常见大实现如下:

Product.h 代码:

class Product
{
public:
    virtual int method() = 0;
};

Factory.h 代码:

class Product;

class Factory
{
public:
    static Product* createProduct(const std::string& name);
};

Factory.cpp 代码:

Product* Factory::createProduct(const std::string& name)
{
    if (name == "ProductType1")
        return new ProductType1;
    else if (name == "ProductType2")
        return new ProductType2;
    // ...
    else
        return NULL;
}

使用代码:

Product* p = Factory::createProduct("ProductType1");

问题

这个实现有几个问题:

  1. 当添加新产品类的时,需要修改代码,在 Factory 中添加相应的代码,这违背了开闭原则。
  2. Factory 类是接口,但是它依赖于具体产品类,这违背了面向接口编程的原则。
  3. 任何产品类的头文件发生改变的时候都会引起 Factory 类重新编译。

为了避免上面的问题,可采取注册的方式来提供具体产品类的信息。

改进实现

新的 Factory.h 代码:

#include <map>
class Product;

class Factory
{
private:
    template <typename T>
    struct construct_helper {
        static Product* create() { return new T(); }
    }


    typedef std::map<std::string, Product*(*)()> Reg;

    static Reg& getRegistry();
public:
    template <typename T>
    static bool reg(const std::string& name) {
        Reg& r = getRegistry();
        r[name] = &construct_helper<T>::create;
        return true;
    }

    static Product* createProduct(const std::string& name);
};

Factory.cpp 的代码:

Factory::Reg& Factory::getRegistry() {
    static Reg reg_;
    return reg_;
}

Product* Factory::createProduct(const std::string& name) {
    const Reg& r = getRegistry();
    Reg::const_iterator it = r.find(name);
    return (it == r.end()) ? NULL : r->second();
}

然后是在具体产品类中添加注册代码, 比如 ProductType1.cpp

#include "ProductType1.h"
#include "Factory.h"

static bool _ = Factory::reg<ProductType1>("ProductType1");

Factory.cpp 中,保存注册信息的变量 reg_ 之所以放在函数 getRegistry 内部而不是放在外面(类里或者文件作用域)的静态变量,是为了保证该变量一定是在被使用前初始化的。因为注册的时候也是利用静态变量初始化完成的,如果注册信息也放在静态变量里面:当使用注册的点的静态变量 ( ProductType1.cpp 中的 _ ) 先初始化,引发对注册信息变量的使用,那么这个时候注册信息变量 reg_ 还没有初始化。

这个改进的实现采用了先通过静态初始化来注册类,然后再通过标识创建对象的时候查找注册信息来早到相应的创建函数。

该实现可进一步将具体产品类的函数声明为私有,并且通过友元对 Factory 类开放。

Lua 常见陷阱

真值定义

问题

在Lua中除了 falsenil 以外的所有值都是真。所以 0 和空字符串 '' 都是真,这和其他语言如 C 、 C++ 有区别。

以下代码打印出 0 is ture.

local v = 0
if v then
    print(v..' is ture')
end

解决方案

多错几次就记住了。

顺便说一句,正因为如此,当 v1 不为 falsenil 时, Lua 表达式 cond and v1 or v2 等效于 C 语言的 cond ? v1 : v2 表达式。

不等号

问题

不等号应该写成 ~= 而不是像大多数语言一样用 !=

解决方案

问题不大,Lua VM 不认识 != 因此是可以检查到的。

未定义的变量当作全局变量

问题

在 Lua 中未定义的变量将被当作全局变量,而且完全合法。

local type = ture
if typo then
    do_some_thing()
end

比如上面的代码由于错误地将 type 写成了 typotypo 被认为是一个全局变量。由于 typo 全局变量没有被赋值。所以为 nil 。因此将不会像想得那样调用 do_some_thing()

解决方案

  1. 使用luacheck这样的静态检查工具,也可配合 Sublime Text 的 SublimeLinter-luacheck 插件或者 Atom 的 linter-luacheck 插件。
  2. 在 Lua 发行包下的 etc 目录有一个 strict.lua 可以用于运行时检查未定义的变量。
  3. 任何变量应该先定义再使用。
  4. 使用可以高亮所有选中变量的编辑器,帮助检查拼写错误。

长度操作符和有洞的表

问题

在 Lua 中,长度操作符用 # 表示,比如 #value

在 Lua 5.1 中表的长度定义为:任何整数索引 n 满足 t[n] 不为 nil 而且 t[n+1]nil 那么 n 即为表 t 的长度。因此对于有洞的表,满足的索引值有多个,而长度操作符的值可能是其中任意一个,具体由 VM 的实现决定。

在 Lua 5.2 和 5.3 中:表的长度仅当表为序列(sequence)的时候才有效。有就是表的正整数键的集合为 {1..n} (n > 1),在这种情况下 n 才是表的长度。像下面的表:

{10, 20, nil, 40}

就不是序列,因为其正整数键的集合为 {1, 2, 4} 。

相应的在 Lua 中:

for i,v in ipairs({10, 20, nil, 40}) do
    -- ...

end

也无法保证能遍历所有正整数键元素。

解决方案

要对表使用长度操作符的时候,务必保证表的数组部分没有洞。

如果不能保证:

  • 需要自己记录最大下标的值。
  • 使用 for i=1,n do ... end 来遍历有洞的数组。

数组下标从 1 开始

问题

在 Lua 中,表的数组部分的下标是从 1 开始的,而其它大多数语言都是从 0 开始。

如果写 t[0] = val 那么它将存储在数组的哈希部分。

同时 ipairs() 只是遍历数组部分,也不是从 0 开始。

解决方案

无完善的解决方案,需要适应和注意;尤其是从其它语言移植代码到 Lua 的时候要特别小心。

OpenGL ES 2.0 渲染管线

最近学习《OpenGL ES 2.0 Prgramming Guide》,对 OpenGL ES 2.0 渲染管线有了一定的了解。

OpenGL ES 的目标之一就是去除多余:任何 Open GL 中多种方式可以实现的相同操作,都将保留其最有用的方式,去掉其他多余的方式。因此在 OpenGL ES 没有固定管线,必须通过 Vertex Shader 和 Fragment Shader 来实现自己的渲染程序。

整个渲染管线如下图所示:

OpenGL ES 中没有 glBegin() glEnd() 的方式来定义顶点数据。只能通过 Vertex Array 或者 Buffer Object 来传递数据给 Vertex Shader 。

Vertex Shader

Vertex shader 的输入包括如下部分:

  • Attributes — 由 vertex arrays 提供的 vertex 数据。
  • Uniforms — vertex shader 使用的常量数据。
  • Samplers — 特殊类型的 uniforms 用于表示给 vertex shader 使用的 textures。Samplers 在 vertex shader 是可选的。
  • Shader program — Vertex shader 程序代码或者可执行程序描述将对顶点执行的操作。

Vertex shader 的输出 Varing Variables 。

Primitive Assembly

主要包括:

  • Clipping - 根据 Primitive 和 View Frustum 的位置关系来裁剪或者完全舍弃。
  • Culling - 根据 Primitive 的朝向来决定是否舍弃。

Rasterization

OpenGL ES 支持 3 种类型的对象的栅格化:

  • 点(Point-Sprite)
  • 线(Line)
  • 三角形(Triangle)

栅格化输出片元(Fragment)供 Fragment Shader 处理。

Fragment Shader

Fragment Shader 包括如下输入部分:

  • Varying variables — Vertex Shader 的输出(多个),被光栅单元插值处理后供 Fragment 使用。
  • Uniforms — Fragment shader 使用的常量数据。
  • Samplers — 特殊类型的 uniforms 用于表示给 fragment shader 使用的 textures。
  • Shader program — Fragment shader 程序代码或者可执行程序描述将对片元执行的操作。

Fragment Shader 输出变量为 gl_FragColor

Pre-Fragment Operations

  1. Pixel ownership test
  2. Scissor test
  3. Stencil test
  4. Depth test
  5. Blending
  6. Dithering

经过以上操作后,片元被最终写入 Framebuffer 。

我是如何写游戏引擎的

背景

首先说明,我写的引擎是在没智能设备的嵌入式环境下。具体就是:

  • 没有OpenGL
  • 没有C++
  • 没有标准C库
  • 只有RTOS
  • 没有在线调试

差不多,就这样子,那种连轮子都要自己发明一遍的平台。

而我写的引擎本身只包括一些基本的功能:

  • 基于C语言的对象系统,支持单继承
  • 场景管理
  • 场景节点树
  • Actions (cocos-2d 那种)
  • 纯软件的2D贴图引擎(支持颜色特效alpha/tint)
  • 动画显示
  • 简单GUI
  • 声音系统接口
  • 文件系统接口
  • Lua配置文件读取
  • 可在PC运行

引擎里面不包括:

  • 物理引擎
  • 线程接口
  • 网络通信

可以看出其实我的游戏引擎并不复杂,总共写下来也就2.3万行C代码。

此外还有数个支持的SDK:

  • 动画格式、编辑工具、编译工具 (能找到不同幀中的相同部分)
  • 高效的压缩图片格式
  • 高效的压缩包格式、打包工具、引擎中的统一用文件接口访问
  • 基于Premake的自动化系统 (类似现在的cocos-2d-x 3.x的cocos工具)

好吧,背景就这些,下面切入正题。

我是如重复发明轮子的

这里只说说我的游戏引擎历程吧。

GML——只能算是API库

从我第一份工作开始的时候,先写了一个射击游戏。然后就开始写游戏的API。那个时候参考了 SDL ,了解的基本的系统接口如何封装。如何跨平台,如何写优秀的代码。所以连我的游戏API库的前缀都叫GML,也算是像SDL致敬吧。

ENS——也是API库

接下来接触了J2ME,学习了相应的API。很喜欢它组织动画的方式。

然后自己业余写了一个叫ENS的游戏API库。风格还是 SDL 式的。同时对自己有如下突破:

  • 期间采用了单元测试,(单元测试真的能找出自己代码中的bug)
  • 采用doxygen输出了漂亮的文档

虽然最终没有写过一款采用 ENS 写的游戏,但是这也算是技术宅的自娱自乐吧。那个时候好像还没有 Github 不然一定会把代码放上面吧。

Horizon——终于能算是引擎了

2008年金融危机的时候,加入了朋友的创业团队。然后开始了我的引擎 Horizon。并用它开发了数个外包项目。

Horizon 的最初出发点是简单。所以一切都从简单出发。但是很快发现它的不足。于是开始接触cocos2d(注意,不是其模仿者iphone版后者C++版),我一下子被它的简洁和使用方便迷住了。印象最深的是他的Action和场景栈。

于是我开始了horizon2,功能就包括了前面讲的那些方面。其中最费力气的,就是动画工具了,因为是嵌入式环境,存储空间小、绘图效率低,所以要尽量找出不同动画帧之间的相同部分,然后分块重用。

此外,还参考了的游戏引擎有 cocos2d-x love2d

其实,也看出来了,实际上要写一款游戏引擎,重要的是找到一款类似的引擎来参考(好吧,就是抄)。

当然其实如果条件允许,还是不要像我这样重复发明轮子的好。我是无奈,因为要运行的目标硬件和系统太差了,不得不重新发明轮子。

另外的学习

其实,开源游戏引擎一大把,无论你是要向我一样发明轮子,还是你想很好的学习和使用一个游戏引擎。这后面需要对很多方面的只是进行学习和了解。比如我觉得最重要的,就是面向对象技术、设计模式等设计和架构方面的知识。

比如Unity3d,如果你使用它,就需要了解组件(Component)模式,这样就能理解其背后的原理。

要了解这些东西,网上有不少资源。比如:

当然上面列的资料只是凤毛麟角,如果你只能记住一条,那么记住 Google 吧。很难想象,一个优秀的程序员,不会翻墙 Google 。

总结

如果你真的要写一款游戏引擎,以我的愚见,你需要:

  • 学习其他引擎是怎么做的,而且了解作者为什么要那样做
  • 学习软件架构方面的知识
  • 翻墙 Google

吐槽一下编辑器

子曰:工欲善其事必先利其器。
我也听说:木匠坐三只脚的凳。

过去

中学时候,在Great Wall Basic上面跑过打印1到10000中每个数的平方,老师还以为我死循环了。那个也算编辑器吗?

大学最初写程序的时候Turbo Pascal,Visual Basic,Visual C++这些IDE伴随我学习语言的过程。但是他们都不是专门的编辑器。后来不务正业,写MUD的程序,语言是LPC(一种C语言变种)。于是换上Edit Plus,印象最深的是他那红色的图标真是漂亮啊。不管怎么说,Edit Plus算是一款真正的编辑器了。

上班后,工作主流还是VC。后来的接触了VisualAssist,感觉为VC插上了翅膀,但是还是不是特别好用。于是我开始思考:到底什么编辑器好?辗转SlickEdit,EmacsVimNotepad++我还是没有找到答案。最终,随着Ruby/Rails的火爆,看到一段使用Textmate的视频。我惊呆了,多选,同时编辑!编辑器还可以这样!可惜的是Textmate是Mac下的专属。还好,后来发现了他的Windows表兄,e-texteditor。那段时间正接触各种脚本和游戏数据编辑,用起来真是舒服高效啊。但是e-texteditor对中文编码支持不是很好。而且目前这个编辑器的域名也易主了,看来开源后就没咋维护了。

现在

时间飞逝,现在我主力使用的编辑器是Sublime Text。跨平台,有Textmate/e-texteditor的多选编辑。而且有不少插件。为了更好的写Markdown,我还花了不少时间为Markdown Preview写了Sublime Text 3的支持。

然后,Github出手了,Atom编辑器来了。遗憾的是目前开发版本在Mac上运行还行。而在Windows,唉吐槽一下,Atom继承了所有Mac转Windows软件的传统,那就是慢的要死。我Sublime Text 3秒开,而Atom要30秒都还不一定打的开。

继续吐槽一下,类似的继承该传统的软件还有:iTunes、iCloud Control Panel、Source Tree。

未来

Atom慢虽然是慢,但是他的的确确是21世纪的编辑器。因为需要更快的硬件嘛,哦不对,是因为他从功能和开发背景来讲超越了Sublime Text。而Sublime Text开发人力少,用户需求根本没有回复。比如之前我有请求新增一个API以方便插件开发。慢是不可怕的,可怕的是支持不力。

所以,Atom,未来是属于你的!

快点!快点!快点!