2019-11-15 17:32:09
UnityGameFramework(UGF) 是由 Ellan Jiang 开发的一个 Unity 游戏开发框架。我决定采用它作为我最近为公司开发的一款 3D 扫雷游戏的开发框架,为此,我觉得有必要仔细阅读它的源码,并做好笔记。另外,我还建了一个仓库去写一些测试代码。
官网已经有教程教用户如何安装框架了。它推荐的安装方式是安装 Unity 插件包,其中核心部分的代码都打包成 DLL 形式了。虽然这种方法方便了用户使用,但我的目的是阅读和调试代码,我得拿到所有的代码。
下面是我的安装方法:
下载某一个版本(如我当前使用的是 v2019.11.09)的 UnityGameFramework,并将它拷贝到新建的 Unity 工程的 Assets
目录之中,如:
UnityGameFramework/Libraries
文件夹下的 GameFramework.dll
和 GameFramework.xml
文件。下载某一个版本的 GameFramework,并将它的源码拷贝到 Unity 工程的 Assets
目录之中。其存放位置任意,如我就将它放进了 UnityGameFramework
目录之中:
然后在 GameFramework
文件夹下新建一个 GameFramework.asmdef
文件:
{
"name": "GameFramework",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode" : true
}
然后让 UnityGameFramework.Runtime.asmdef
依赖 GameFramework.asmdef
,让 UnityGameFramework.Editor.asmdef
同时依赖 GameFramework.asmdef
和 UnityGameFramework.Runtime.asmdef
即可。
{
"name": "UnityGameFramework.Runtime",
"references": [
"GameFramework"
],
"includePlatforms": [],
"excludePlatforms": []
}
{
"name": "UnityGameFramework.Editor",
"references": [
"UnityGameFramework.Runtime",
"GameFramework"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": []
}
如此,整个框架就安装好了。接下来我们可以新建一个空场景 LaunchScene.unity
作为我们的游戏的启动场景,然后把框架提供的 GameFramework.prefab
拖入到场景中:
现在点击运行按钮就可以让框架代码跑起来了。
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$,然后再加上新增的用户投票计算一个算术平均分。这是贝叶斯推断的一个与直觉相悖的特点,即用后验统计作为先验条件。
首先看如何获取豆瓣图书的数据。搜索图书的 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 中,我们不能直接通过 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)
。