Logo

site iconAlexander D Huang

游戏工程师,住在深圳。大学专业是应用物理,仍然对物理和数学保持着浓厚的兴趣。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Alexander D Huang RSS 预览

UGF 源码阅读笔记:(1)安装

2019-11-15 17:32:09

UnityGameFramework(UGF) 是由 Ellan Jiang 开发的一个 Unity 游戏开发框架。我决定采用它作为我最近为公司开发的一款 3D 扫雷游戏的开发框架,为此,我觉得有必要仔细阅读它的源码,并做好笔记。另外,我还建了一个仓库去写一些测试代码。

官网已经有教程教用户如何安装框架了。它推荐的安装方式是安装 Unity 插件包,其中核心部分的代码都打包成 DLL 形式了。虽然这种方法方便了用户使用,但我的目的是阅读和调试代码,我得拿到所有的代码。

下面是我的安装方法:

  • 下载某一个版本(如我当前使用的是 v2019.11.09)的 UnityGameFramework,并将它拷贝到新建的 Unity 工程的 Assets 目录之中,如:

  • 删除 UnityGameFramework/Libraries 文件夹下的 GameFramework.dllGameFramework.xml 文件。
  • 下载某一个版本的 GameFramework,并将它的源码拷贝到 Unity 工程的 Assets 目录之中。其存放位置任意,如我就将它放进了 UnityGameFramework 目录之中:

    然后在 GameFramework 文件夹下新建一个 GameFramework.asmdef 文件:

      {
          "name": "GameFramework",
          "references": [],
          "includePlatforms": [],
          "excludePlatforms": [],
          "allowUnsafeCode" : true
      }
    

    然后让 UnityGameFramework.Runtime.asmdef 依赖 GameFramework.asmdef,让 UnityGameFramework.Editor.asmdef 同时依赖 GameFramework.asmdefUnityGameFramework.Runtime.asmdef 即可。

      {
          "name": "UnityGameFramework.Runtime",
          "references": [
              "GameFramework"
          ],
          "includePlatforms": [],
          "excludePlatforms": []
      }
    
      {
          "name": "UnityGameFramework.Editor",
          "references": [
              "UnityGameFramework.Runtime",
              "GameFramework"
          ],
          "includePlatforms": [
              "Editor"
          ],
          "excludePlatforms": []
      }
    

如此,整个框架就安装好了。接下来我们可以新建一个空场景 LaunchScene.unity 作为我们的游戏的启动场景,然后把框架提供的 GameFramework.prefab 拖入到场景中:

现在点击运行按钮就可以让框架代码跑起来了。

How to Implement a Tag Archive Page in Jekyll

2019-11-12 17:31:47

Jekyll uses Liquid, which was created by Shopify and written in Ruby, as its template language. For making a tag archive page, we have to do a little bit of dirty work because of the lack of power of Liquid. But first, let me describe what a tag page we want to make here.

On the top of the tag archive page, there is a list of labels, for example,

Ignoring the style, we can see that each label has a tag name and the count of tags. Besides, each label has a link to the section of the related posts list. These labels are first sorted by the counts and then by the alphabetic.

Talk is cheap. Show me the code.

OK, I show you the code now:


{% assign tags_max = 0 %}
{% for tag in site.tags %}
  {% if tag[1].size > tags_max %}
    {% assign tags_max = tag[1].size %}
  {% endif %}
{% endfor %}

{% assign tag_names_array = "" %}
{% assign tag_counts = "" %}
{% assign first_array_element = true %}
{% for i in (1..tags_max) reversed %}
  {% assign tag_names = "" %}
  {% assign first_tag = true %}

  {% for tag in site.tags %}
    {% if tag[1].size == i %}
      {% if first_tag %}
        {% assign first_tag = false %}
      {% else %}
        {% assign tag_names = tag_names | append: "," %}
      {% endif %}
      {% assign tag_names = tag_names | append: tag[0] %}
    {% endif %}
  {% endfor %}

  {% if tag_names != "" %}
    {% assign tag_names = tag_names | split: "," | sort | join: "," %}

    {% if first_array_element %}
      {% assign first_array_element = false %}
    {% else %}
      {% assign tag_names_array = tag_names_array | append: "|" %}
      {% assign tag_counts = tag_counts | append: "|" %}
    {% endif %}
    {% assign tag_names_array = tag_names_array | append: tag_names %}
    {% assign tag_counts = tag_counts | append: i %}
  {% endif %}
{% endfor %}

{% assign tag_names_array = tag_names_array | split: "|" %}
{% assign tag_counts = tag_counts | split: "|" %}


<ul class="taxonomy-index">
  {% for tag_names in tag_names_array %}
    {% assign tag_names_list = tag_names | split: "," %}
    {% assign tag_count = tag_counts[forloop.index0] %}
    {% for tag_name in tag_names_list %}
      <li>
        <a href="#{{ tag_name | slugify }}">
          <strong>{{ tag_name }}</strong> <span class="taxonomy-count">{{ tag_count }}</span>
        </a>
      </li>
    {% endfor %}
  {% endfor %}
</ul>

It looks dirty, so let us walk through the code for better understanding.


{% assign tags_max = 0 %}
{% for tag in site.tags %}
  {% if tag[1].size > tags_max %}
    {% assign tags_max = tag[1].size %}
  {% endif %}
{% endfor %}

This segment is for calculating the maximum counts of tags. {% site.tags %} is a hash of posts indexed by the tag, for example,

{ 
    'tech' => [<Post A>, <Post B>],
    'ruby' => [<Post B>] 
}

Then we define two strings tag_names_array and tag_counts. What we want to have are two arrays, but by the lack of syntax for directly creating arrays in Liquid, we play a trick here. We use a long string to collect tag names; each element is delimited by a vertical line |, and each tag name in each element is delimited by a comma ,. For example,

"tech,ruby|jekyll|html,css,javascript"

Similarly, we also use a string to collect tag counts; each count is delimited by a vertical line |.

Next, we define an auxiliary Boolean value first_array_element. If the first element is appended to the array, it will be set to false. It is used to check whether a delimiter | should be appended to the array.

Next, we iterate from tags_max to 1, and inside this loop, we define two variables tag_names and first_tag. Their roles are similar with tag_names_array and tag_counts. Then we create an inner loop to find all tags whose count is matched with i:


{% for tag in site.tags %}
  {% if tag[1].size == i %}
    {% if first_tag %}
      {% assign first_tag = false %}
    {% else %}
      {% assign tag_names = tag_names | append: "," %}
    {% endif %}
    {% assign tag_names = tag_names | append: tag[0] %}
  {% endif %}
{% endfor %}

After escaping this loop, if tag_names is not an empty string, that means we have collected tags whose counts are equal to i. So we append tag_names to tag_names_array, and at the same time, append i to tag_counts.


{% if tag_names != "" %}
  {% assign tag_names = tag_names | split: "," | sort | join: "," %}
  {% if first_array_element %}
    {% assign first_array_element = false %}
  {% else %}
    {% assign tag_names_array = tag_names_array | append: "|" %}
    {% assign tag_counts = tag_counts | append: "|" %}
  {% endif %}
  {% assign tag_names_array = tag_names_array | append: tag_names %}
  {% assign tag_counts = tag_counts | append: i %}
{% endif %}

Now we can make two real arrays by calling split:


{% assign tag_names_array = tag_names_array | split: "|" %}
{% assign tag_counts = tag_counts | split: "|" %}

Until now, all the things we do are prepare works. Let’s do a real job: showing the list of labels.


<ul class="taxonomy-index">
  {% for tag_names in tag_names_array %}
    {% assign tag_names_list = tag_names | split: "," %}
    {% assign tag_count = tag_counts[forloop.index0] %}
    {% for tag_name in tag_names_list %}
      <li>
        <a href="#{{ tag_name | slugify }}">
          <strong>{{ tag_name }}</strong> <span class="taxonomy-count">{{ tag_count }}</span>
        </a>
      </li>
    {% endfor %}
  {% endfor %}
</ul>

At last, we need to show the post entries for each tag:


{% for tag_names in tag_names_array %}
  {% assign tag_names_list = tag_names | split: "," %}
  {% for tag_name in tag_names_list %}
    <section id="{{ tag_name | slugify | downcase }}" class="taxonomy-section">
      <h2 class="taxonomy-title">{{ tag_name }}</h2>
      {% for tag in site.tags %}
        {% if tag[0] == tag_name %}
          <div>
            {% for entry in tag.last %}
              {% comment %} Show the entry of each post in the style you like. {% endcomment %} 
            {% endfor %}
          </div>
        {% endif %}
      {% endfor %}
    </section>
  {% endfor %}
{% endfor %}

Since we have finished a tag archive page, I think a category archive page is also easy to make by little modifications.

基于贝叶斯平均对豆瓣图书的搜索结果进行排序

2019-10-09 16:59:33

想必有人会对豆瓣图书搜索结果的排序感到困惑吧?举个例子,假设我们搜索“JavaScript”,我们会发现排在第 3 位的是《JavaScript DOM编程艺术 (第2版)》,豆瓣评分为 8.7,有 1505 人评价;但是排在第 4 位的《JavaScript语言精粹》的豆瓣评分更高,为 9.1,而且评价人数更多,有 1792 人。更糟糕的情况是,我们想要找的高分图书往往出现在好几页之后。因此,我开发了一个 Web 应用,它基于贝叶斯平均对搜索结果进行排序。

为什么用贝叶斯平均?

如果我只想对搜索到的图书按评分排序,为什么不直接用豆瓣的评分,而用贝叶斯平均分?

原因之一是,我实在不知道豆瓣评分是怎么计算的!在此,我只能假设它就是一个简单的算术平均。现在我们假设某本书 A 有一个人评价,评分为 9.5;而某本书 B 有 100 个人评价,算术平均分为 8.9。哪一本书应当排在前面?哪一本书更值一读?算术平均不能回答这些问题。

什么是贝叶斯平均分?它的公式为

其中 $x_i$ 为某一投票项的某人给出的评分,$n$ 为某一投票项的投票人数;$C$ 是一个与数据集大小成正比的数,我们可以令它等于每一个投票项的平均投票人数;$m$ 为每一个投票项的预设评分,我们可以令它等于总体投票人给出的评分的算术平均值。因此,贝叶斯平均 $\bar{x}$ 是一个随着投票人数的增加而不断修正的值。

它的意义也很容易看出,即相当于我们预先给每个投票项投了 $C$ 张票,每张票的评分为 $m$,然后再加上新增的用户投票计算一个算术平均分。这是贝叶斯推断的一个与直觉相悖的特点,即用后验统计作为先验条件。

通过豆瓣 Open API 获取数据

首先看如何获取豆瓣图书的数据。搜索图书的 API 为

https://api.douban.com/v2/book/search?q=[keywords]

其中参数 q 为搜索的关键词。

它以 JSON 格式返回数据,类似于

{
    "count": 20,
    "start": 0,
    "total": 199,
    "books": [
        { ... },
        { ... },
        ...
    ]
}

其中 count 为本页所含的数据条目数,start 为本页所含的数据条目的起始索引,total 为总的数据条目数,books 为本页数据条目列表。

搜索 API 可以带上参数 &start=[start],则其返回的数据条目从索引 [start] 开始。

JavaScript 实现

现在我们可以编写 JavaScript 程序去计算豆瓣图书的贝叶斯平均分了。

由于跨域访问的限制,在 JavaScript 中,我们不能直接通过 HTTPRequest 访问豆瓣 API,但是我们可以用 JSONP 技术。

/**
 * @param url 访问的 URL
 * @param callback 回调函数的名字,在此函数中我们处理 url 返回的 JSON 数据
 */ 
function jsonp(url, callback)
{
    const script = document.createElement("script");
    script.type = "text/javascript";
    script.src = url + `&callback=${callback}`;
    script.async = true;
    document.body.append(script);
}

我们的回调函数命名为 jsonpCallback,在此函数中,我们解析、缓存数据,并最终计算出每一本书的贝叶斯平均分。

// 收集到的图书数据列表
let books = [];

// 读取的最大图书数据条目数量
const MAX_BOOKS = 2048;

// 已经读取的图书数据条目数量
let booksRead = 0;

// 搜索 URL
let searchURL;

function jsonpCallback(page)
{
    const count = page['count'];
    const start = page['start'];
    const total = page['total'] > MAX_BOOKS ? MAX_BOOKS : page['total'];

    if (total == 0) return;

    if (start == 0) {
        booksRead = 0;
        // 遍历每一页数据
        for (let s = start + count; s < total; s += count) {
            jsonp(searchURL + `&start=${s}`, "jsonpCallback");
        }
    }

    page['books'].forEach(book => {
        // 只读取评分大于 0 的图书
        if (parseFloat(book['rating']['average']) > 0) {
            books.push(book);
        }
    });

    booksRead += count;

    // 已读取的条目数达到了总数,解析数据完毕,可以计算贝叶斯平均了
    if (booksRead >= total) {
        // 计算每一本书的贝叶斯平均分
        calculateBayesian(books);
        // 依贝叶斯平均分从大到小排序
        books.sort((a, b) => {
            return b['rating']['bayesian'] - a['rating']['bayesian'];
        });
        // 展示结果给用户查看
        showBooks();
    }
}

计算贝叶斯平均的代码也就是上面所提的公式的翻译了:

function calculateBayesian(books) {
    let allRaters = 0;
    let allScore = 0;

    for (let book of books) {
        const n = book['rating']['numRaters'];
        allRaters += n;
        allScore += n * parseFloat(book['rating']['average']);
    }

    const C = allRaters / books.length;
    const m = allScore / allRaters;

    for (let book of books) {
        const n = book['rating']['numRaters'];
        book['rating']['bayesian'] = (C * m + n * parseFloat(book['rating']['average'])) / (C + n);
    }
}

最后再封装一下:

function sortBooks(keywords)
{
    books = [];

    searchURL = `${api}?q=${encodeURI(keywords)}`;
    jsonp(searchURL, "jsonpCallback");
}

用户输入 keywords,点击“搜索”按钮,即调用 sortBooks(keywords)