普通视图

发现新文章,点击刷新页面。
昨天以前崎径 其镜赵安琪的博客

从零配置 VS Code C++ 环境

作者 Anqi Zhao
2026年4月13日 06:32

本文档记录在 Windows 系统上从零配置 VS Code C++ 开发环境的全过程,包括编译、调试和运行。

前置条件

需要的工具

1. MSYS2

MSYS2 是一个 Windows 下的 Linux-like 环境,提供 g++ 编译器和 gdb 调试器。

2. VS Code 扩展

  • C/C++ 扩展:由 Microsoft 提供,支持 IntelliSense、调试等功能
    • 在 VS Code 中搜索安装:ms-vscode.cpptools

安装步骤

步骤 1:安装 MSYS2

  1. 下载 MSYS2 安装包并运行安装程序
  2. 选择安装路径为 C:\msys64
  3. 安装完成后,运行 MSYS2 UCRT64 终端(从开始菜单启动)

步骤 2:更新 MSYS2 并安装工具

在 MSYS2 UCRT64 终端中执行以下命令:

1
2
3
4
5
# 更新包数据库
pacman -Syu

# 安装 g++ 编译器和 gdb 调试器
pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-gdb

步骤 3:安装 VS Code 扩展

  1. 打开 VS Code
  2. Ctrl+Shift+X 打开扩展面板
  3. 搜索 C/C++ 并安装 Microsoft 的 C/C++ 扩展

VS Code 配置

在你的 C++ 项目根目录(例如 C:\Dev\leetcode)创建 .vscode 文件夹,并添加以下配置文件:

1. c_cpp_properties.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"configurations": [
{
"name": "windows-gcc-x64",
"includePath": [
"${workspaceFolder}/**"
],
"compilerPath": "C:\\msys64\\ucrt64\\bin\\gcc",
"cStandard": "${default}",
"cppStandard": "${default}",
"intelliSenseMode": "windows-gcc-x64",
"compilerArgs": [
""
]
}
],
"version": 4
}

2. settings.json

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
53
54
55
56
57
58
59
60
61
{
"C_Cpp.default.compilerPath": "c:\\msys64\\ucrt64\\bin\\gcc.exe",
"C_Cpp_Runner.cCompilerPath": "gcc",
"C_Cpp_Runner.cppCompilerPath": "g++",
"C_Cpp_Runner.debuggerPath": "gdb",
"C_Cpp_Runner.cStandard": "",
"C_Cpp_Runner.cppStandard": "",
"C_Cpp_Runner.msvcBatchPath": "C:/Program Files/Microsoft Visual Studio/VR_NR/Community/VC/Auxiliary/Build/vcvarsall.bat",
"C_Cpp_Runner.useMsvc": false,
"C_Cpp_Runner.warnings": [
"-Wall",
"-Wextra",
"-Wpedantic",
"-Wshadow",
"-Wformat=2",
"-Wcast-align",
"-Wconversion",
"-Wsign-conversion",
"-Wnull-dereference"
],
"C_Cpp_Runner.msvcWarnings": [
"/W4",
"/permissive-",
"/w14242",
"/w14287",
"/w14296",
"/w14311",
"/w14826",
"/w44062",
"/w44242",
"/w14905",
"/w14906",
"/w14263",
"/w44265",
"/w14928"
],
"C_Cpp_Runner.enableWarnings": true,
"C_Cpp_Runner.warningsAsError": false,
"C_Cpp_Runner.compilerArgs": [],
"C_Cpp_Runner.linkerArgs": [],
"C_Cpp_Runner.includePaths": [],
"C_Cpp_Runner.includeSearch": [
"*",
"**/*"
],
"C_Cpp_Runner.excludeSearch": [
"**/build",
"**/build/**",
"**/.*",
"**/.*/**",
"**/.vscode",
"**/.vscode/**"
],
"C_Cpp_Runner.useAddressSanitizer": false,
"C_Cpp_Runner.useUndefinedSanitizer": false,
"C_Cpp_Runner.useLeakSanitizer": false,
"C_Cpp_Runner.showCompilationTime": false,
"C_Cpp_Runner.useLinkTimeOptimization": false,
"C_Cpp_Runner.msvcSecureNoWarnings": false,
"terminal.external.windowsExec": "C:\\Windows\\System32\\cmd.exe"
}

3. tasks.json

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
53
54
55
56
{
"version": "2.0.0",
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: g++.exe build active file",
"command": "C:\\msys64\\ucrt64\\bin\\g++.exe",
"args": [
"-fdiagnostics-color=always",
"-g",
"${file}",
"-o",
"${workspaceFolder}\\build\\Debug\\outDebug.exe"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "Compile current C++ file to build/Debug/outDebug.exe"
},
{
"type": "shell",
"label": "Run C++ Program",
"command": "cmd.exe",
"args": [
"/c",
"start \"\" cmd.exe /k \"${workspaceFolder}\\build\\Debug\\outDebug.exe\""
],
"dependsOn": [
"C/C++: g++.exe build active file"
],
"options": {
"shell": {
"executable": "C:\\Windows\\System32\\cmd.exe"
}
},
"group": {
"kind": "test",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "shared"
},
"problemMatcher": []
}
]
}

4. launch.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"version": "0.2.0",
"configurations": [
{
"name": "C/C++ Runner: Debug Session",
"type": "cppdbg",
"request": "launch",
"args": [],
"stopAtEntry": false,
"externalConsole": false,
"cwd": "c:/Dev/leetcode",
"program": "${workspaceFolder}/build/Debug/outDebug.exe",
"MIMode": "gdb",
"miDebuggerPath": "C:\\msys64\\ucrt64\\bin\\gdb.exe",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}

5. keybindings.json(全局配置)

在 VS Code 用户设置中添加快捷键绑定(Ctrl+Shift+P -> Preferences: Open Keyboard Shortcuts (JSON)):

1
2
3
4
5
6
7
8
9
10
[
{
"key": "f6",
"command": "workbench.action.tasks.runTask",
"args": {
"task": "Run C++ Program"
},
"when": "editorTextFocus"
}
]

使用方法

  1. 编译:按 Ctrl+Shift+B 或打开命令面板运行 “Tasks: Run Build Task”
  2. 调试:按 F5,会启动 gdb 调试器
  3. 运行:按 F6,会打开 cmd 黑框窗口运行程序

常见问题

1. 找不到 g++ 或 gdb

  • 确保 MSYS2 已正确安装并更新
  • 检查 PATH 环境变量是否包含 C:\msys64\ucrt64\bin

2. 编译失败

  • 检查代码语法错误
  • 确保头文件路径正确

3. 调试失败

  • 确保已编译生成 build/Debug/outDebug.exe
  • 检查 gdb 路径是否正确

4. 黑框不弹出

  • 重启 VS Code
  • 检查 terminal.external.windowsExec 设置

版本信息

  • MSYS2: 最新版本
  • g++: 15.2.0
  • gdb: 17.1
  • VS Code: 最新版本
  • C/C++ 扩展: 最新版本

参考资料

Unity 游戏的 Google Play 16 kb页面对齐处理

作者 Anqi Zhao
2026年4月4日 01:21

16 KB 内存页面大小的支持,是Google Play 新提出的要求。要在2026年5月31日之前,满足这一条件:

https://play.google.com/console/u/0/developers/9060101706093336387/app/4974577679306649072/policy-center/issues/4988764664083295045/details

Android 15 的 16KB page 检测主要看三件事:

  1. PT_LOAD >= 16 KB
  2. RELRO 必须在 segment 末尾(suffix)
  3. RELRO end 必须 16KB 对齐

对于aab 来说,上面的三个要求,一般只用关注PT_LOAD。因为aab 有压缩率,所以关于压缩的两项检验直接计算是不准确的。Google Play 在分发时会自动处理。

官方文档:

https://developer.android.com/guide/practices/page-sizes?utm_source=chatgpt.com&hl=zh-cn

检测方式

关于so 文件是否合规的检测,主要有以下几种方法:

方法一

可以解压出so文件,然后使用ndk 工具检测(一定使用PowerShell,cmd无法运行):

1
llvm-objdump.exe -p E:\Test\so\arm64-v8a\libanogs.so | Select-String -Pattern "LOAD"

运行结果可以看LOAD off 类型行尾是不是带有212、213。
低于2**14的so文件都不符合。

工具的路径在:

1
{YourNDKPath}\toolchains\llvm\prebuilt\windows-x86_64\bin

但是这里只检查了PT_LOAD。

方法二

Google 官方提供了检测工具:

https://cs.android.com/android/platform/superproject/main/+/main:system/extras/tools/check_elf_alignment.sh?hl=zh-cn

可以直接使用这个脚本进行检测。

1
check_elf_alignment.sh APK_NAME.apk

方法三

最后,如果将Android Studio 更到最新版本,即可使用Apk Analyze 功能进行检测。

前者报错为:

1
4 KB LOAD section alignment, but 16 KB is required

后者报错为:

1
RELRO is not a suffix and its end is not 16 KB aligned

适配方法

Unity 相关so

Unity相关的so,需要通过提升Unity 编辑器版本号的方式进行解决。

第三方SDK

第三方SDK 相关的so,需要联系相关提供商进行SDK 升级。

如果无法完成合规,应该考虑取消对该插件的接入。

自己编译的so

如果是自己使用NDK 编译的so,需要升级NDK工具并在编译中指定相关参数。

1
2
3
4
5
6
7
({NDK Path}/ndk-build ^
NDK_PROJECT_PATH=. ^
APP_BUILD_SCRIPT=Android.mk ^
NDK_APPLICATION_MK=Application.mk ^
APP_ABI="armeabi-v7a x86 arm64-v8a x86_64" ^
APP_LDFLAGS="-Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384" ^ // 新增
|| pause) && pause

对于Android.mk 文件和Application.mk 文件也有内容需要修改:

如果旧内容涉及cmd-strip,在打包时会报错。这是因为NDK r23+中,strip 工具路径变成 LLVM 版本。最简单的解决方式是删掉这一行,新版本不需要手动调用strip,会自动调用。

如果文件中存在:APP_STL := gnustl_static,也需要进行修改。
修改为:APP_STL := c++_static

为了兼容RELRO 必须在 segment 末尾(suffix)和 RELRO end 必须 16KB 对齐,需要在Android.mk 中增加:

1
2
LOCAL_LDFLAGS += -Wl,-z,max-page-size=16384
LOCAL_LDFLAGS += -Wl,-z,common-page-size=16384

最后,在旧的代码中,如果使用了PAGE_SIZE / PAGE_MASK 宏,这会报错,在新版本中,NDK 不再提供。
需要增加以下内容:

1
2
3
4
5
6
7
#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif

#ifndef PAGE_MASK
#define PAGE_MASK (~(PAGE_SIZE - 1))
#endif

3. il2cpp处理方式

升级到新版Unity之后,如果不修改代码,可能不会让C++代码重新导出。

也可以删除Library目录下的Bee目录和所有的il2cpp_*目录的缓存,强制生成。

一文详解Hexo 博客搭建

作者 Anqi Zhao
2026年4月3日 22:06

前言

捡起了好久没有更新的博客。

在AI 的帮助下,我完成了整个环境的重新搭建。

由于我使用的hexo 框架和版本都比较老了,所以有些地方可能不适应于新版本的主题,在接入时还请注意。

参考视频: https://www.bilibili.com/video/BV1xTgTemEDU/

视频文档: https://xiamu-ssr.github.io/Hexo/2024/06/19/2024-H1/2024-06-19-12-31-52/

准备工作

使用Github Actions 服务,将hexo 部署到 Github Pages。

与参考内容不同的是,我使用了两个仓库来实现。

参考内容中,使用了同一个项目,两个分支。其中一个分支用于提交博客源文件,另一个分支用于存放生成的静态网页文件。

考虑到将博客源文件内容暴露出来会有一定的风险,于是我的方案是使用两个仓库来实现。其中,Private 的仓库用于存放博客源文件,Public 的仓库用于存放页面。

我之前是有hexo 环境的,会和从头开始实现有所区别,在接下来的内容中,我将会分别说明。

关于Github Actions 服务的用量,Public 仓库可以免费使用,但是Private 的仓库会有一定的限制。但是如果只是作为hexo的资源站,免费的额度也是可以覆盖的。

hexo准备

安装并初始化hexo,如果已有hexo环境,可跳过这一步。

1
2
3
4
npm install -g hexo-cli
hexo init blog
cd blog
npm install

如果是已有hexo环境,在其他地方做过备份的。可以删除node_modules目录和package-lock.json,然后重新安装:

1
2
cd blog
npm install

仓库准备

在github上新建两个仓库,一个Private的blog_base,一个Public 的username.github.io;

已有Github Pages 的,可以不用重新创建username.github.io,只创建博客源文件仓库。

Token准备

使用Github Actions ,需要生成Personal access tokens,而且至少要包含repo 权限。把这个Token 配置给Github Action, 它才有权限去执行自动部署。

首先是Token 的创建:

单击头像 -> Settings -> Developer Settings -> Personal access tokens -> Tokens (classic)

这里是所有Tokens的列表,下面我们继续创建:

Generate new token -> Generate new token (classic)

新打开的界面中,我们需要给这个token 命名,随便命名即可,比如blog_base。

在下面的权限列表中,勾选repo,workflow 两项。

记录token的有效期,有效期到期后需要给Github Actions 配置新的token。

完成创建后,去到blog_base 仓库进行配置。首先打开该仓库,点击上方的Settings -> Secrets and variables -> Actions

点击 New repository secret 创建,Name设置为:GH_TOKEN(可随意设置,后面保持一致即可),Secret 粘贴刚才生成出来的token内容,就是一串以ghp_为开头的字符串。

修改_config.yml

为了设置推送的地址,需要在_config.yml 中配置推送地址。这里选择Public 的Github pages 仓库地址:

1
2
3
4
deploy:
type: git
repo: https://github.com/yourusername/your-repo.git
branch: master

配置Github Actions 工作流

在blog_base 目录创建.github文件夹,再在.github文件夹下创建workflows文件夹,然后创建deploy.yml,内容如下:

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
name: Deploy Hexo to GitHub Pages

on:
push:
branches:
- main # 当推送到 main 分支时触发

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
submodules: false # 禁用子模块检查

- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '12'

- name: Install Dependencies
run: npm install

- name: Install Hexo Git Deployer
run: |
npm install hexo-deployer-git --save
npm install hexo-cli -g

- name: Clean and Generate Static Files
run: |
hexo clean
hexo generate

- name: Configure Git
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'

- name: Deploy to GitHub Pages
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
cd public/
git init
git add -A
git commit -m "Create by workflows"
git branch -M master
git remote add origin https://${{ secrets.GH_TOKEN }}@github.com/yourusername/your-repo.git
git push -f origin master

这里最后一步是将生成的内容,推送到你配置的仓库的master 分支,我的Github Pages就是设置的这个分支。

配置里的node-version 这个参数需要注意。旧版的hexo 使用新版的node 可能会生成失败。我的主题使用的hexo 版本比较老,使用12版本是可以生成的,最新的24 反而无法生成,这里可以按需修改版本。

这个工作流的意思就是,使用ubuntu-latest作为基础环境,然后安装各种依赖,随后hexo generate生成博客网站静态文件夹,最后再推送到设定的仓库和分支。

配置Github Pages

到Github Pages 仓库,点击Settings,设置page来源的分支。

推送并查看是否能够生效。

在这里,介绍一下我的.gitignore文件:

1
2
3
4
5
6
7
.DS_Store
Thumbs.db
db.json
*.log
public/
.deploy*/
node_modules/

.DS_Store文件和Thumbs.db文件。这两个文件分别是Mac系统和Win系统生成的垃圾。

public 目录存放的是本地生成的静态页面,这个也不必上传。Github Actions 会自动生成。

node_modules 目录一定不能上传,需要排除掉。因为它太大了,不方便版本控制。而且不同平台需求的可能都不同。本地环境和运行Github Actions 的虚拟机环境不可能保证一致的。

其他注意事项

如果你之前是有Github Pages的,需要做好备份,防止文件丢失。

我就出现了CNAME文件和其他文件丢失的情况,这里需要注意。

自定义域名

如果嫌username.github.io 的网址不够优雅,可以注册域名,然后配置解析。

在购买域名后,进行以下设置:

Github 配置

进入Github Pages 仓库,单击Settings 切页。

在左侧边栏点击Pages。

Custom domain 配置为:

1
www.yourdomin.xxx

然后勾选Enforce HTTPS

域名解析

在域名解析中,做出以下设置:

1
2
3
4
5
6
7
8
www  CNAME  yourusername.github.io
@ AAAA 2606:50c0:8003::153
@ AAAA 2606:50c0:8002::153
@ AAAA 2606:50c0:8001::153
@ AAAA 2606:50c0:8000::153
@ A 185.199.111.153
@ A 185.199.110.153
@ A 185.199.108.153

这里参考Github的文档:https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site

创建CNAME

需要在Github Pages 网站的根目录创建CNAME文件,内容是在第一步中配置的www.yourdomin.xxx

一文详解Hexo 博客搭建

作者 Anqi Zhao
2026年4月3日 22:06

前言

捡起了好久没有更新的博客。

在AI 的帮助下,我完成了整个环境的重新搭建。

由于我使用的hexo 框架和版本都比较老了,所以有些地方可能不适应于新版本的主题,在接入时还请注意。

参考视频: https://www.bilibili.com/video/BV1xTgTemEDU/

视频文档: https://xiamu-ssr.github.io/Hexo/2024/06/19/2024-H1/2024-06-19-12-31-52/

准备工作

使用Github Action 服务,将hexo 部署到 Github Pages。

与参考内容不同的是,我使用了两个仓库来实现。

参考内容中,使用了同一个项目,两个分支。其中一个分支用于提交博客源文件,另一个分支用于存放生成的静态网页文件。

考虑到将博客源文件内容暴露出来会有一定的风险,于是我的方案是使用两个仓库来实现。其中,Private 的仓库用于存放博客源文件,Public 的仓库用于存放页面。

我之前是有hexo 环境的,会和从头开始实现有所区别,在接下来的内容中,我将会分别说明。

1. hexo准备

安装并初始化hexo,如果已有hexo环境,可跳过这一步。

1
2
3
4
npm install -g hexo-cli
hexo init blog
cd blog
npm install

如果是已有hexo环境,在其他地方做过备份的。可以删除node_modules目录和package-lock.json,然后重新安装:

1
2
cd blog
npm install

2. 仓库准备

在github上新建两个仓库,一个Private的blog_base,一个Public 的username.github.io;

已有Github Pages 的,可以不用重新创建username.github.io,只创建博客源文件仓库。

3. Token准备

使用Github Actions ,需要生成Personal access tokens,而且至少要包含repo 权限。把这个Token 配置给Github Action, 它才有权限去执行自动部署。

首先是Token 的创建:

单击头像 -> Settings -> Developer Settings -> Personal access tokens -> Tokens (classic)

这里是所有Tokens的列表,下面我们继续创建:

Generate new token -> Generate new token (classic)

新打开的界面中,我们需要给这个token 命名,随便命名即可,比如blog_base。

在下面的权限列表中,勾选repo,workflow 两项。

记录token的有效期,有效期到期后需要给Github Actions 配置新的token。

完成创建后,去到blog_base 仓库进行配置。首先打开该仓库,点击上方的Settings -> Secrets and variables -> Actions

点击 New repository secret 创建,Name设置为:GH_TOKEN(可随意设置,后面保持一致即可),Secret 粘贴刚才生成出来的token内容,就是一串以ghp_为开头的字符串。

4. 修改_config.yml

为了设置推送的地址,需要在_config.yml 中配置推送地址。这里选择Public 的Github pages 仓库地址:

1
2
3
4
deploy:
type: git
repo: https://github.com/yourusername/your-repo.git
branch: master

5. 配置Github Actions 工作流

在blog_base 目录创建.github文件夹,再在.github文件夹下创建workflows文件夹,然后创建deploy.yml,内容如下:

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
name: Deploy Hexo to GitHub Pages

on:
push:
branches:
- main # 当推送到 main 分支时触发

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
submodules: false # 禁用子模块检查

- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '12'

- name: Install Dependencies
run: npm install

- name: Install Hexo Git Deployer
run: |
npm install hexo-deployer-git --save
npm install hexo-cli -g

- name: Clean and Generate Static Files
run: |
hexo clean
hexo generate

- name: Configure Git
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'

- name: Deploy to GitHub Pages
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
cd public/
git init
git add -A
git commit -m "Create by workflows"
git branch -M master
git remote add origin https://${{ secrets.GH_TOKEN }}@github.com/yourusername/your-repo.git
git push -f origin master

这里最后一步是将生成的内容,推送到你配置的仓库的master 分支,我的Github Pages就是设置的这个分支。

配置里的node-version 这个参数需要注意。旧版的hexo 使用新版的node 可能会生成失败。我的主题使用的hexo 版本比较老,使用12版本是可以生成的,最新的24 反而无法生成,这里可以按需修改版本。

这个工作流的意思就是,使用ubuntu-latest作为基础环境,然后安装各种依赖,随后hexo generate生成博客网站静态文件夹,最后再推送到设定的仓库和分支。

6. 配置Github Pages

到Github Pages 仓库,点击Settings,设置page来源的分支。

推送并查看是否能够生效。

7. 其他注意事项

如果你之前是有Github Pages的,需要做好备份,防止文件丢失。

我就出现了CNAME文件和其他文件丢失的情况,这里需要注意。

8. 自定义域名

如果嫌username.github.io 的网址不够优雅,可以注册域名,然后配置解析。

在购买域名后,进行以下设置:

8.1 Github 配置

进入Github Pages 仓库,单击Settings 切页。

在左侧边栏点击Pages。

Custom domain 配置为:

1
www.yourdomin.xxx

然后勾选Enforce HTTPS

8.2 域名解析

在域名解析中,做出以下设置:

1
2
3
4
5
6
7
8
www  CNAME  yourusername.github.io
@ AAAA 2606:50c0:8003::153
@ AAAA 2606:50c0:8002::153
@ AAAA 2606:50c0:8001::153
@ AAAA 2606:50c0:8000::153
@ A 185.199.111.153
@ A 185.199.110.153
@ A 185.199.108.153

这里参考Github的文档:https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site

8.3 创建CNAME

需要在Github Pages 网站的根目录创建CNAME文件,内容是在第一步中配置的www.yourdomin.xxx

xlua学习笔记

作者 Anqi Zhao
2021年5月19日 03:05

文件加载

使用LuaEnv中的DoString方法来执行Lua脚本。
因为创建一个LuaEnv相当于创建了一个lua虚拟机,所以一个游戏最好只有一个LuaEnv。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 执行lua代码
env.DoString("print(\"Hello World\")");

// 通过文件执行lua代码
TextAsset ta = Resources.Load<TextAsset>("Lua/helloworld.lua");
env.DoString(ta.text);

// 通过默认Loader执行lua代码
env.DoString("require 'Lua/helloworld'");

// 通过自定义Loader执行lua代码
// require实际上是调一个个的Loader去加载,有一个成功就不再往下尝试,全失败则报文件找不到。
private byte[] LuaLoader(ref string filePath)
{
string path = "Lua/" + filePath + ".lua";
print(path);
TextAsset ta = Resources.Load<TextAsset>(path);
return System.Text.Encoding.UTF8.GetBytes(ta.text);
}

env.AddLoader(LuaLoader);
env.DoString("require 'helloworld'");

官方建议的加载Lua脚本方式是:整个程序就一个DoString(“require ‘main’”),然后在main.lua加载其它脚本(类似lua脚本的命令行执行:lua main.lua)。

C#访问Lua普通全局变量

先加载lua脚本,再通过LuaEnv.Global.Get获取变量。

1
2
3
4
5
6
TextAsset ta = Resources.Load<TextAsset>("Lua/variable.lua");
env.DoString(ta.text);

int g_int = env.Global.Get<int>("g_int");
string g_str = env.Global.Get<string>("g_str");
bool g_bool = env.Global.Get<bool>("g_bool");

C#访问Lua table的四种方法

需要注意的是,在构建C#中对应的结构时,接口一定要是public修饰,否则会报错。类最好也是,但是经测试,没有报错。

映射到普通class或struct

table的属性可以多于或者少于class的属性,也就是不一定都需要做映射。这个过程是值拷贝,如果class比较复杂代价会比较大。而且修改class的字段值不会同步到table,反过来也不会。

1
2
3
4
5
6
7
public class Person
{
public string name;
public int age;
}

Person p = env.Global.Get<Person>("g_tbl");

映射到接口

一定要加[CSharpCallLua]和public,否则会报错。

1
2
3
4
5
6
7
g_int = 1
g_str = "Hello World Variable"
g_bool = false
g_tbl = {name = "Juhnny", age = 12}
g_tbl.say = function(this, str)
print(str)
end

1
2
3
4
5
6
7
8
9
10
[CSharpCallLua]
public interface IPerson
{
string name { get; set; }
int age { get; set; }
public void say(string str);
}

IPerson p = env.Global.Get<IPerson>("g_tbl");
p.say("Yahoo");

映射到Dictionary和List

这种方法较方法2更为轻量级,但是要求table下key和value类型一致。

1
2
g_dic = {apple = "iPhone", flower = "Huawei"}
g_list = {1, 3, 5, 7, 9}

1
2
3
4
5
6
7
8
9
10
11
Dictionary<string, string> dic = env.Global.Get<Dictionary<string, string>>("g_dic");
foreach (var child in dic)
{
print(string.Format("Key is {0}, Value is {1}", child.Key, child.Value));
}

List<int> list = env.Global.Get<List<int>>("g_list");
foreach (var v in list)
{
print(v);
}

映射到LuaTable

这种方法比较慢,而且没有类型检查,不推荐使用。

1
2
3
4
5
6
7
8
LuaTable tab = env.Global.Get<LuaTable>("g_tbl");
string name = tab.Get<string>("name");
int age = tab.Get<int>("age");
print(string.Format("Name is {0}, age is {1}", name, age));
foreach (string key in tab.GetKeys())
{
print(tab.Get<object>(key));
}

C#访问Lua全局函数

使用delegate映射

这种是建议的方式,性能好很多,而且类型安全,但是要生成代码。支持带返回值,多返回值,甚至返回值都可以是delegate。

1
2
3
4
5
6
7
8
g_func1 = function()
print("FF")
end

g_func2 = function(num)
print(num)
return num, "Yoshida", "SE"
end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[CSharpCallLua]
public delegate int Func2(int num, out string name, out string company);
// 声明委托时,一定要加public和[CSharpCallLua]

// 无返回
Action func1 = env.Global.Get<Action>("g_func1");
func1();
func1 = null;

// 单返回值
Func2 func2 = env.Global.Get<Func2>("g_func2");
int num = func2(14);
func2 = null

// 多返回值
Func2 func2 = env.Global.Get<Func2>("g_func2");
string name;
string company;
int num = func2(14, out name, out company);
func2 = null

在过去的一些版本中,需要在不使用后,将接收到的函数销毁,否则会在LuaEnv.Dispose的时候报错。

使用LuaFunction

与上一种方法优缺点相反,不推荐使用。

1
2
LuaFunction func = env.Global.Get<LuaFunction>("g_func2");
object[] res = func.Call(14);

Lua调用C

1
2
3
4
5
6
7
8
9
10
11
// 读静态属性
CS.UnityEngine.Time.deltaTime

// 写静态属性
CS.UnityEngine.Time.timeScale = 0.5

// 调用静态方法
CS.UnityEngine.GameObject.Find('helloworld')

// 读写成员属性类似,但是访问成员方法要使用:
testobj:DMFunc()

其它注意事项

方法的参数处理
Lua调用侧的参数处理规则:C#的普通参数算一个输入形参,ref修饰的算一个输入形参,out不算,然后从左往右对应lua 调用侧的实参列表。

Lua调用侧的返回值处理规则:C#函数的返回值(如果有的话)算一个返回值,out算一个返回值,ref算一个返回值,然后从左往右对应lua的多返回值。

可变参数的处理

1
2
3
4
5
// 对于C#的如下方法:
void VariableParamsFunc(int a, params string[] strs)

// 可以在lua里头这样调用:
testobj:VariableParamsFunc(5, 'hello', 'john')

枚举

1
2
3
4
5
6
7
8
9
10
// 枚举值就像枚举类型下的静态属性一样。

testobj:EnumTestFunc(CS.Tutorial.TestEnum.E1)

上面的EnumTestFunc函数参数是Tutorial.TestEnum类型的。

枚举类支持__CastFrom方法,可以实现从一个整数或者字符串到枚举值的转换,例如:

CS.Tutorial.TestEnum.__CastFrom(1)
CS.Tutorial.TestEnum.__CastFrom('E1')

委托

1
2
3
4
5
6
7
8
-- 使用+-号来实现委托
-- 比如testobj里头有个事件定义是这样:public event Action TestEvent;

-- 增加事件回调
testobj:TestEvent('+', lua_event_callback)

--移除事件回调
testobj:TestEvent('-', lua_event_callback)

Lua新知

作者 Anqi Zhao
2021年5月10日 19:42

两年没有碰过Lua了,总觉得得要捡起来。现在从头到尾再过一遍,总结一些东西。之前根据菜鸟学的时候,使用了5.1.1的版本。与现在常用的版本相悖。因此这次我使用了:zerobrane studio,它可以方便切换版本,能实现很好的练习效果。

关于多行注释的安全问题

如果直接使用默认的–[[–]]来进行多行注释的时候,如果遇到table包table的情况会直接结束注释:

1
2
3
4
5
6
--[[
a = [];
b = [a];
a[b[1]]
a = 1
--]]

可以使用另一种方法来进行多行注释,避免这个问题:

1
2
3
4
5
6
--[=[
a = [];
b = [a];
a[b[1]]
a = 1
]=]

当然,最安全的方法还是使用IDE的快捷键进行注释.IDE会在每行的前面加单行注释,避免错误的发生.

多行字符串安全问题

多行字符串,也会出现类似于上面的情况:

1
2
3
4
5
6
7
8
9
10
str = 
[[
话说
这个东西
好像打印不出来
就是这个
table[table[idx]]
咋办啊
]]
print(str)

运算符相关

#运算符取的是最大索引的值,如果删除了中间的值,获得的结果不会改变:

1
2
3
4
5
tb_a = {1, 2, 3}
print (#tb_a)

tb_a[2] = nil
print (#tb_a)

但是这种情况仅限于Lua 5.1 ,Lua5.3和Lua5.4结果是1。

模拟三目运算符的and…or…,在第二个参数为false时,始终返回c,会出现问题:

1
2
a = true
print (a and false or true)

输出结果会是true 而不是三目运算符应该返回的false。
这时候,可以将三目运算符的后面两个部分转换成table,运算结束后再取第一个元素,即可实现:

1
2
3
4
5
6
7
a = true
b = (a and {false} or {true})[1]
if b then
print("true")
else
print("false")
end

自定义迭代器

自定义迭代器的格式:

1
2
3
for 变量列表 in 迭代函数, 状态变量, 控制变量 do
-- 循环体
end

自定义无状态迭代器

1
2
3
4
5
6
7
8
9
10
11
function square(iteratorMaxCount, currentNumber)
if currentNumber < iteratorMaxCount then
currentNumber = currentNumber + 1
return currentNumber, currentNumber * currentNumber
end
end

for i, n in square, 3, 0
do
print(i, n)
end

多状态迭代器:
多个状态可以存放在table,因此只需要一个参数即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
array = {"Google", "Runoob"}

function elementIterator (collection)
local index = 0
local count = #collection
-- 闭包函数
return function ()
index = index + 1
if index <= count then
-- 返回迭代器的当前元素
return collection[index]
end
end
end

for element in elementIterator(array) do
print(element)
end

循环时的goto语句

可以使用goto语句来实现continue:

1
2
3
4
5
6
7
8
9
for i=1, 3 do
if i <= 2 then
print(i, "yes continue")
goto continue
end
print(i, " no continue")
::continue::
print([[i'm end]])
end

元表

Lua可以通过设置元表,定义元方法的方式,改变table的一些行为:

index元方法,可以修改按索引取值的逻辑。
index内容是一个表的话,如果table里没有值,会从元表中对应的索引去取:

1
2
3
4
5
6
7
8
9
10
11
t_table = setmetatable({k1 = "v1"}, {__index = {k2 = "v2"}})
print(t_table["k1"])
print(t_table["k2"])
t_table["k2"] = "v3"
print(t_table["k2"])
--[[
输出是:
v1
v2
v3
--]]

若__index内容是一个函数,则可以定义返回逻辑:

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
t_table = setmetatable({}, {__index =
function(t_table, key)
if key == 1 then
return "true"
else
return "false"
end
end
})

print(t_table[1])
print(t_table[2])
print(t_table[3])

t_table = {1, 2, 3}

print(t_table[1])
print(t_table[2])
print(t_table[3])
--[[
输出是:
true
false
false
1
2
3
--]]

__newindex元方法,可以修改追加索引时的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
t_table = setmetatable({k1 = "v1"}, {
__newindex =
function(t_table, key, value)
rawset(t_table, key, "\"v" .. value .. "\"")
end
})

t_table["k2"] = 2

print(t_table["k1"])
print(t_table["k2"])

--[[
输出是:
v1
"v2"
--]]

其它的规则:
|元表|运算符|
|:–:|:–:|
|add|+|
|
sub|-|
|mul|*|
|
div|/|
|mod|%|
|
unm|-(相反数)|
|concat|..|
|
eq|==|
|lt|<|
|
le|<=|

add和tostring:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
t_table = setmetatable({1, 2, 3}, {__add =
function(t_table, data)
t_table[#t_table + 1] = data
return t_table
end,
__tostring =
function(t_table)
local res = ""
for _, v in pairs(t_table) do
res = res .. v
end
return res
end
})
print(t_table)
print(t_table + 4)
--[[
输出:
123
1234
--]]

需要注意的是,关系运算符,比较的两端,元表必须相同。如果只有一方有元表,另一方没有,又或者是两方拥有不同的元表,会导致比较报错:

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
a_metatable =
{
__gt = function(a_table, b_table)
return a_table[1] > b_table[1]
end,
__lt = function(a_table, b_table)
return a_table[1] < b_table[1]
end
}
b_metatable =
{
__gt = function(a_table, b_table)
return b_table[1] > a_table[1]
end,
__lt = function(a_table, b_table)
return b_table[1] < a_table[1]
end
}
a_table = setmetatable({0}, a_metatable)
b_table = setmetatable({1}, b_metatable)
print(a_table<b_table)
--[[
报错内容是:
attempt to compare two table values
--]]

重写取反操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
a_metatable =
{
__unm = function(a_table)
local _temp_table = {}
for i = #a_table, 1, -1 do
_temp_table[#a_table + 1 - i] = a_table[i]
end
return _temp_table
end
}
p_metatable =
{
__tostring =
function(a_table)
local res = ""
for _, v in pairs(a_table) do
res = res .. v
end
return res
end
}
a_table = setmetatable({1, 2, 3}, a_metatable)
b_table = setmetatable(-a_table, p_metatable)
print(b_table)

__call比较有意思,可以让table可以变得和函数一样可以执行。因此可以写成如下的极其过分的形式:

1
2
3
4
5
6
7
8
9
10
function_content =
{
__call = function(a_table, a_data)
print(a_data)
end
}


func_print = setmetatable({}, function_content)
b = func_print(4)

协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function sleep(n)
local t0 = os.clock()
while os.clock() - t0 <= n do end
end
-- 这里的sleep会占用大量资源 正常逻辑不能使用

test_coroutine = coroutine.create(
function()
for i = 1, 10, 1 do
print(i)
sleep(0.1)
coroutine.yield()
end
end
)

function do_cro(co)
while(coroutine.status(co) ~="dead") do
coroutine.resume(co)
end
end

do_cro(test_coroutine)

面向对象

用table实现了类与继承

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
-- 类
-- 类管理器
CClassManager = {}
function CClassManager.ctor(cls, ...)
local this = {}
setmetatable(this, cls)
cls.__index = cls -- 将元表的__index设为自身,访问表的属性不存在时会搜索元表
cls.init(this, ...)
return this
end

function CClassManager.VInit(self, ...)
end

function CClassManager.init(self, ...)
self:VInit(...)
end

function CClassManager.dtor(cls)
-- do sth
end


-- 通过继承实现的类的定义
ClassT = CClassManager:ctor()
function ClassT.VInit(self, word)
self.word = word
end

function ClassT.say(self)
print(self.word)
end

test = ClassT:ctor("B")
test:say()
test:dtor()
test:say()

使用贝塞尔曲线实现道具随机飞动效果

作者 Anqi Zhao
2020年12月13日 05:03

工作中,遇到了一个需求是要实现获得道具和货币的飞动效果:

  1. 根据道具的多少生成不同数目的道具
  2. 货币首先要有一个炸开的效果,然后向一个点汇集;道具从原位置沿直线飞过去
  3. balabala

这里只讨论货币飞行路线的问题。

贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。PS中的钢笔工具就是使用贝塞尔曲线来绘制矢量曲线的。

这里使用简单的,四个点确定的贝塞尔曲线来实现飞行轨迹。首先p0和p3分别是移动的起点和终点。为了模拟爆炸效果,可以通过随机选取以p0为圆心,长度为R的圆上的点,调整曲线的第一次弯折位置实现。这一点定为P1。最后,根据P1点和P0点之间的关系,进行x轴或者y轴的修正,实现曲线平滑延申到p3,这一点是p2点。

p1点(爆炸点)的获取方式:

1
2
3
4
5
private static Vector3 GetRandomPosition(Vector3 vec)
{
float sita = UnityEngine.Random.Range(-Mathf.PI, Mathf.PI);
return new Vector3(vec.x + Mathf.Cos(sita) * Screen.width / 8, vec.y + Mathf.Sin(sita) * Screen.width / 8);
}

p2点(修正点)的获取方式:

1
2
3
4
private static Vector3 GetFixedPoint(Vector3 start, Vector3 end)
{
return new Vector3(start.x + 3*(end.x - start.x)/4, start.y + 3 * (end.y - start.y) / 4 + p1.y > start.y ? Screen.height / 15 : - Screen.height / 15);
}

根据已知的四个点,实现物体延贝塞尔曲线移动的效果。

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
/// <summary>
/// 延贝塞尔曲线移动
/// </summary>
private static IEnumerator MoveBezier(GComponent gcom, float time, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3)
{
for (float t = 0; t < time; t += Time.deltaTime)
{
gcom.position = CalculateCubicBezierPoint(t / time, p0, p1, p2, p3);
yield return 0;
}
gcom.position = p3;
gcom.Dispose();
}

/// <summary>
/// 计算贝塞尔曲线点的坐标
/// </summary>
private static Vector3 CalculateCubicBezierPoint(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3)
{
float u = 1 - t;
float tt = t * t;
float uu = u * u;
float uuu = uu * u;
float ttt = tt * t;

Vector3 p = uuu * p0;
p += 3 * uu * t * p1;
p += 3 * u * tt * p2;
p += ttt * p3;

return p;
}

震惊,JS不加分号会造成错误!?

作者 Anqi Zhao
2019年12月2日 08:12

在之前的工作中,我遇到了一个奇怪的问题。明明在语法上没有问题,找同事也看了,但是程序依旧会产生奇怪的错误。最后通过一步一步断点,定为了错误位置,才找到了造成这个错误的原因———在一个不需要分号的语言中,句末不加分号居然报错了。

不卖关子了,这个错误是多返回值函数造成了对上一句值的影响。下面举个例子:

1
2
3
4
5
6
7
8
9
10
function func() {
return [1, 2]
}

let b = 0
let c = 0
let a = 3
[b, c] = func();
console.log("a is :" + a)
console.log("b is :" + b + " c is :" + c)

运行的结果将会是这样:

a is :1,2
b is :0 c is :0

很容易看到,我们函数的返回值给了上一行的a。这是因为我们的编译器将代码认为了:

1
let a = 3 ,[b, c] = func();

避免这种情况,还是要多加分号吧——虽然它并不会报错。

Linux升级Python

作者 Anqi Zhao
2019年11月28日 20:12

11月底,腾讯云搞了一波双11返场活动,我买了三年的服务器。

和买了新的电脑或者做了新系统一样,得先把生产环境搞好。

距离Python2.x停止维护大概只有5个月了吧,所以第一要务是升级Python的版本。但是yum是依赖Python2的,所以升级还是会有一些顾虑的。下面是升级的过程:

下载、解压

1
2
wget https://www.python.org/ftp/python/3.8.0/Python-3.8.0.tgz
tar -zxvf Python-3.8.0.tgz

安装编译环境

1
2
yum install make build-essential libssl-dev zlib1g-dev libbz2-dev
yum install zlib

编译、安装

1
2
3
4
5
6
yum install -y make build-essential libssl-dev zlib1g-dev libbz2-dev
yum -y install zlib

cd Python-3.8.0
./configure
make && make install

备份与配置

1
2
mv /usr/bin/python /usr/bin/python.bak
ln -s /usr/local/bin/python3 /usr/bin/python

保证yum可用

将下面文件中的配置修改为Python2.x版本的路径。

1
2
/usr/bin/yum
/usr/libexec/urlgrabber-ext-down

安装pip

pip需要依赖setuptools,所以要先安装setuptools。

1
2
3
4
5
6
7
8
9
10
11
wget https://files.pythonhosted.org/packages/ce/1d/96320b9784b04943c924a9f1c6fa49124a1542039ce098a5f9a369227bad/setuptools-42.0.1.zip
unzip setuptools-42.0.1.zip
cd setuptools-42.0.1
python setup.py build
python setup.py install

wget https://files.pythonhosted.org/packages/ce/ea/9b445176a65ae4ba22dce1d93e4b5fe182f953df71a145f557cffaffc1bf/pip-19.3.1.tar.gz
tar -zxvf pip-19.3.1.tar.gz
cd pip-19.3.1
python setup.py build
python setup.py install

参考资料

https://www.cnblogs.com/zhangym/p/6226435.html
https://www.cnblogs.com/fjping0606/p/9156344.html
https://www.cnblogs.com/fyly/p/11112169.html

Github图床工具

作者 Anqi Zhao
2019年11月18日 02:32

这是一个上传图片到github的工具,目前还不是很成熟,不过已经可以实现压缩并上传图片的目的了,对于写博客来说已经够用了。

使用步骤

  1. 创建todo目录
  2. 创建pic目录,在github上创建一个空项目,然后pull到这里
  3. 修改配置文件,将git_url修改为上一步新建的项目
  4. 将图片放到todo文件夹下
  5. 执行工具

需要注意的是,因为图床需要一个git目录,同时代码也需要。在使用部分git管理工具时会禁止这种目录的嵌套。因此最好将代码独立运行。可以自己打包,也可以使用我打好的可执行程序。

在这里需要提一句,一般如果只想要下载github上项目的一个目录或者一个文件,可以使用svn进行下载。将文件路径中的文件名/master替换为trunk即可使用下载。

经过配置之后,以后只需要将图片放到todo目录下,执行脚本即可。

编译时需要安装以下运行库

1
2
3
pip install pyyaml
pip install gitpython
pip install pillow

更新文档

Ver.0.2 2019-11-17 提交编译的可执行程序;增加配置文件

Ver.0.1 2019-11-12 提交项目

JS使用replace()函数全部替换

作者 Anqi Zhao
2019年11月17日 06:03

在处理爬虫爬取下来的数据时,遇到了在文字中出现了经过转义的换行符,在文中显示出了\n,很影响观赏效果。因此,我对内容做了处理。

但是在刷库的过程中,我发现,我总不能一次处理完所有的数据。后来发现是JavaScript的Replace函数的问题,这个函数默认只能替换第一个匹配到的项目。如果需要处理全部的,需要使用正则表达式:

1
string.replace(/\\n/g, "\n")

除此之外,下面是一些处理爬取内容常用操作,包含了html的转义:

1
2
3
4
5
6
7
8
9
10
string.replace(/&nbsp;/g, ' ')
string.replace(/&lt;/g, '<')
string.replace(/&gt;/g, '>')
string.replace(/&amp;/g, '&')
string.replace(/&quot;/g, '"')
string.replace(/&#x3D;/g, '=')
string.replace(/\[.*?\]/g,'')
string.replace("\\n","\n")
string.replace("\\t","")
string.replace("\\r","")

JS使用Splice()函数操作数组

作者 Anqi Zhao
2019年11月17日 05:59

在js的使用过程中,有一次需要对数组进行各种操作,一时间迫使我想要去使用链表。后来通过查阅资料,总结了下面的一些方法,主要使用了splice()函数。

下面的方法主要是使用下标进行操作。如果是用值的话,可以通过indexOf()函数来获取下标。若不存在则返回-1。

值交换

1
2
3
4
function swap_arr(a_list, index1, index2) {
a_list[index1] = a_list.splice(index2, 1, a_list[index1])[0];
return a_list;
}

置顶

1
2
3
4
5
function up_arr(a_list, index){
    if(index!=0 && index!=-1){
a_list[index] = a_list.splice(index-1, 1, a_list[index])[0];
    }
}

下移

1
2
3
4
5
function down_arr(a_list, index) {
    if(index!=a_list.length-1 && index!=-1){
a_list[index] = a_list.splice(index+1, 1, a_list[index])[0];
    }
}

插入

1
2
3
4
5
6
7
8
9
10
11
function ins_arr(a_list, index, a_data) {
if(index!=-1){
if (a_list.indexOf(a_data)==-1) {
a_list.splice(index+1, 0, a_data);
return true;
}
else {
return false;
}
}
}

在顶部插入

1
2
3
function topins_arr(a_list, a_data) {
a_list.splice(0, 0, a_data)
}

删除元素

1
2
3
4
5
6
7
8
function del_arr(a_list, a_data_list) {
for (const ele of a_data_list) {
let index = a_list.indexOf(ele);
if(index!=-1){
a_list.splice(index, 1);
}
}
}

当你的程序连接Mysql然后崩溃时

作者 Anqi Zhao
2019年11月17日 05:43

之前写过一个监控mysql数据库更新状态的预警程序,总是莫名其妙的报一个连接错误的错,然后程序死掉。后来在系统趋于稳定之后,我就没再继续维护这个工具了。

但是最近我在写另一个工具时,遇到了一个奇怪的问题,就是:tick总在27000多左右的时候崩溃。

我进行了一系列的猜测,比如tick的代码,或者是逻辑有问题,最后我把思路放在了之前遇到的这个错误上。查阅资料后发现,MySQL数据库在连接之后,如果超过一个设定的时间戳之后,会断开。这个值叫WAIT_TIMEOUT,默认值是28800,也就是说如果连上MySQL数据库之后,8小时内没有进行操作,这个连接便会断开。

网上很多连接MySQL数据库的代码没有处理过超时连接的问题,就连JS的官方代码好像也是在17年之后才更新的。以往这个问题,大家都是通过修改这个值来进行规避的。比如改成300天。修改有两种方式,一种是修改配置文件,这样在启动时便会使用这个配置;另一种是修改这个值,或者全局,或者当次生效。

下面是我在使用JavaScript语言链接MySQL数据库时,处理超时重连问题的代码:

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
this.config = {
"host": "x.x.x.x",
"port": xxxx,
"user": "root",
"password": "pass",
"database": "name""
}

async connect() {
let self = this;
console.log("connect mysql success with", JSON.stringify(this.config))
// 创建连接
this.db_mysql = mysql.createConnection(
this.config
);
// 连接数据库
await this.db_mysql.connect();
// 错误处理
this.db_mysql.on('error', function(err) {
if (err) {
if (err.code === 'PROTOCOL_CONNECTION_LOST') {
// 处理超时
console.warning("start reconnect mysql");
self.connect();
} else {
console.error(err.stack || err);
console.warning("start reconnect mysql");
self.connect();
}
}
});
}

安卓应用闪屏

作者 Anqi Zhao
2019年11月17日 05:19

去年在接入安卓SDK时,会有部分渠道有要求手写闪屏的情况,下面是当时的笔记,这只是最简单的一种方法。

参考资料:

很好的例子:

https://www.jianshu.com/p/a609f510b19a

https://blog.csdn.net/l799069596/article/details/47094731

安卓动画:https://blog.csdn.net/IO_Field/article/details/53101499

背景:

除去游戏本身的闪屏之外,有的渠道会要求,有额外的渠道闪屏。为了使用一套资源出不同渠道包,我们可以对接渠道的AS工程进行处理,单独设置闪屏。

首先,创建一个闪屏Activity,为你的主Activity,这样在游戏的一开始你就可以看到闪屏了。

这里需要注意的是,你原先的Activit也需要在Manifest中注册打开日志,否则在打包的时候会找不到,报错:

https://blog.csdn.net/qq_28301007/article/details/52265775

1
<activity android:name="...Activity"/>

下面是主Activity,也就是闪屏Activity的代码,需要根据AS的提示import缺少的部分。

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
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.view.WindowManager;
import android.widget.ImageView;

public class SplashActivity extends Activity {
private final int TimeAnimDurning = 2000;
private int displayDeviceWidth;
ImageView iv_splash;
private ObjectAnimator objAnim;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
displayDeviceWidth = getResources().getDisplayMetrics().widthPixels;
setContentView(R.layout.activity_splash);
iv_splash = (ImageView) findViewById(R.id.splash);
objAnim = ObjectAnimator.ofFloat(iv_splash,"alpha",1,0);
objAnim.setDuration(TimeAnimDurning);

new Handler().postDelayed(new Runnable() {
@Override
public void run() {
objAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if((int)animation.getAnimatedFraction() == 1){
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
startActivity(new Intent(SplashActivity.this , .YouActivity.class));
finish();
return ;
}
}
});
objAnim.start();
}

}, 2000);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context="com.unity3d.player.SplashActivity">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible">

<ImageView
android:id="@+id/splash"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:scaleType="fitXY"
android:src="@drawable/splash" />

</LinearLayout>
</RelativeLayout>

在上面控制闪屏格式的style.xml中,可以看到闪屏的背景色设置为了白色。这里有一些常用的颜色xml:https://blog.csdn.net/sundaysunshine/article/details/53509854

除了这两处之外,还需要根据style.xml中的配置,放好闪屏图片,设置闪屏背景。

整个闪屏的原理就是创建一个动画,在动画播放完成之后,去执行一个新的activity。在补全报错的部分之后,还是有一些细节部分需要注意的。

首先是结束时间的判定。判定时机总共有两种,一种是获取动画的进度,就像这里的例子,使用(int)animation.getAnimatedFraction()进行获取一个从0~1的数,来表示目前的动画的播放进度。除此之外还可以获取播放的时间,这个函数是:getAnimatedValue(),它可以获取属性的当前值。使用这两个函数可以很方便地控制动画的时间和动作。

除此之外,在调起另一个Activity之后,我结束了这个Activity。这是因为如果使用默认的LaunchMode,在重新唤醒应用时,闪屏会再次启动,然后走完动画,应用重启。这就造成了应用无法关闭的状况,只能后台强制杀掉。解决办法就是让闪屏只执行一次。

U3D问题总结(六) 优化

作者 Anqi Zhao
2019年10月30日 07:00

请简述GC(垃圾回收)产生的原因,并描述如何避免(?

GC回收堆上的内存
避免:1.减少new产生对象的次数
2.使用公用的对象(静态成员)
3.将String换为StringBuilder

如何优化内存?

1.压缩自带类库;
2.将暂时不用的以后还需要使用的物体隐藏起来而不是直接Destroy掉;
3.释放AssetBundle占用的资源;
4.降低模型的片面数,降低模型的骨骼数量,降低贴图的大小;
5.使用光照贴图,使用多层次细节(LOD),使用着色器(Shader),使用预设(Prefab)。
6.代码中少产生临时变量

UNITY3d在移动设备上的一些优化资源的方法

1.使用assetbundle,实现资源分离和共享,将内存控制到200m之内,同时也可以实现资源的在线更新
2.顶点数对渲染无论是cpu还是gpu都是压力最大的贡献者,降低顶点数到8万以下,fps稳定到了30帧左右
3.只使用一盏动态光,不是用阴影,不使用光照探头
粒子系统是cpu上的大头
4.剪裁粒子系统
5.合并同时出现的粒子系统
6.自己实现轻量级的粒子系统
animator也是一个效率奇差的地方
7.把不需要跟骨骼动画和动作过渡的地方全部使用animation,控制骨骼数量在30根以下
8.animator出视野不更新
9.删除无意义的animator
10.animator的初始化很耗时(粒子上能不能尽量不用animator)
11.除主角外都不要跟骨骼运动apply root motion
12.绝对禁止掉那些不带刚体带包围盒的物体(static collider )运动
NUGI的代码效率很差,基本上runtime的时候对cpu的贡献和render不相上下
13每帧递归的计算finalalpha改为只有初始化和变动时计算
14去掉法线计算
15不要每帧计算viewsize 和windowsize
16filldrawcall时构建顶点缓存使用array.copy
17.代码剪裁:使用strip level ,使用.net2.0 subset
18.尽量减少smooth group
19.给美术定一个严格的经过科学验证的美术标准,并在U3D里面配以相应的检查工具

场景优化

1.遮挡剔除(Occlusion Culling) 不显示被遮挡住的物体
2.LOD 根据相机距离远近显示不同精细程度的模型
3.大场景可以调节相机可视距离
4.小物体可以适当隐藏掉
5.使用光照贴图 避免动态实时的进行光照计算,提高效率

UI优化

1.将同一画面图片放到同一图集中
2.图片和文字尽量不要交叉,会产生多余drawcall(相同材质和纹理的UI元素是可以合并的)
3.UI层级尽量不要重叠太多
4.取消勾选不必要的射线检测RaycastTarget
5.将动态的UI元素和静态的UI元素放在不同的Canvas中,减少canvas网格重构频率

GC优化

1.字符串使用StringBuilder而不是string,stringBuilder在创建时会自动获取一个容量存储并逐渐扩充,string每一次改变都会创建一个新的对象。
2.访问物体tag的时候尽量使用Gameobject.CompareTag(),因为访问物体的tag属性会在堆上额外的分配空间
3.使用对象池缓存大量创建的物体
4.用for代替foreach,foreach每次迭代产生24字节垃圾内存

U3D问题总结(五) 渲染与光照

作者 Anqi Zhao
2019年10月30日 07:00

什么是渲染管道(?

是指在显示器上为了显示出图像而经过的一系列必要操作。 渲染管道中的很多步骤,都要将几何物体从一个坐标系中变换到另一个坐标系中去。主要步骤有:
本地坐标->视图坐标->背面裁剪->光照->裁剪->投影->视图变换->光栅化

对渲染管线的理解

渲染流水线流程:
1.应用阶段(由CPU负责,输出是渲染所需要的几何信息,即渲染图元。然后发起Draw Call,进入GPU流水线)
2.几何阶段(由GPU负责,处理渲染图元,这阶段中最重要的就是把顶点坐标变换屏幕空间中,交给光栅器处理
这阶段输出的是屏幕空间中二维顶点坐标、每个顶点对应的深度值、着色等相关信息)
3.光栅化阶段(由GPU,负责这一阶段会使用上个阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像)
细节过程https://zhuanlan.zhihu.com/p/97498781

什么是DrawCall?DrawCall高了又什么影响?如何降低DrawCall?

Unity中,每次引擎准备数据并通知GPU的过程称为一次Draw Call。DrawCall越高对显卡的消耗就越大。降低DrawCall的方法:

  1. Dynamic Batching
  2. Static Batching
  3. 高级特性Shader降级为统一的低级特性的Shader。

什么是DrawCall,有什么方法可以减少DrawCall

CPU通过调用绘制命令来告诉GPU开始进行一个渲染过程(一次DrawCall)。
CPU方面减少DrawCall:
1、使用Draw Call Batching 、Dynamic Batching动态批处理
2、纹理打包成图集减少材质使用
3、少用反光、阴影
4、设置一个合适的Fixed Timestep
5、不要使用网格碰撞器(Mesh Collider)
6、大量或频繁的字符串连接操作一定要用StringBuilder
7、某些可能情况,使用结构体代替类
8、使用对象池重复利用空间
9、尽量不要用foreach,用for
10、不要直接访问GameObjcet的tag属性
11、不要频繁使用GetComponent,访问一次后保留其引用
12、使用OnBecameVisible()和OnBecameInVisible(),控制物体update()函数的执行减少开销
13、使用内建数组,如Vector3.zero而不是new Vector3(0,0,0)
14、使用ref关键字对方法的参数进行优化
15、关闭所有update中的log操作
16、不在update中调用GetComponent、SendMessage、FindWithTag等方法
17、不在update中使用临时变量

GPU方面减少DrawCall

1、使用纹理图集代替一系列单独小贴图
2、保持材质数目尽可能少
3、如果使用纹理图集和共享材质,用Renderer.sharedMaterial代替Renderer.material
4、使用光照纹理(lightmap)而非实时灯光
5、使用LOD
6、使用mobile版的shader
7、尽可能减少顶点数、背面删减
8、压缩图片,减少显存带宽压力

什么是material,什么是shader,二者有什么关系

材质系统定义了如何渲染物件表面信息。shader里面使用材质信息加自身操作,最终呈现物体渲染。shader是material一部分,是根据计算即时演算生成贴图的程序,叫着色器。常用处理无法用固定贴图表现的模型。material是模型的材质,包含贴图、shader、顶点、凹凸等信息。

如何在Unity3D中查看场景的面数,顶点数和Draw Call数?如何降低Draw Call数

在Game视图右上角点击Stats。降低Draw Call 的技术是Draw Call Batching
这个在5.0以后在window-》Profiler下面,快捷键是cmd + 7(ctl + 7

DrawCall和SetPass Call

DrawCall:meshes网格绘制应用批处理后的总数。请注意,在多次呈现对象(例如,由像素灯照明的对象),每个在一个单独的渲染结果绘制调用。

SetPass Call:渲染改变( passes)次数。每个改变 需要Unity运行时绑定一个新的渲染器(shader),它可能会引入 CPU 开销。

Unity3D Shader分哪几种,有什么区别

表面着色器的抽象层次比较高,它可以轻松地以简洁方式实现复杂着色。表面着色器可同时在前向渲染及延迟渲染模式下正常工作。
顶点片段着色器可以非常灵活地实现需要的效果,但是需要编写更多的代码,并且很难与Unity的渲染管线完美集成。
固定功能管线着色器可以作为前两种着色器的备用选择,当硬件无法运行那些酷炫Shader的时,还可以通过固定功能管线着色器来绘制出一些基本的内容。

有A和B两组物体,有什么办法能够保证A组物体永远比B组物体先渲染?

把A组物体的渲染队列大于B物体的渲染队列,通过shader里面的渲染队列来渲染

问一个Terrain,分别贴3张,4张,5张地表贴图,渲染速度有什么区别?为什么?

没有区别,因为不管几张贴图只渲染一次。

LOD是什么,优缺点(?

LOD(Level of detail)多层次细节,是最常用的游戏优化技术。它按照模型的位置和重要程度决定物体渲染的资源分配,降低非重要物体的面数和细节度,从而获得高效率的渲染运算。缺点是增加了内存。

MipMap是什么,作用(?

MipMapping:在三维计算机图形的贴图渲染中有常用的技术,为加快渲染进度和减少图像锯齿,贴图被处理成由一系列被预先计算和优化过的图片组成的文件,这样的贴图被称为MipMap。

什么是LightMap

LightMap:就是指在三维软件里实现打好光,然后渲染把场景各表面的光照输出到贴图上,最后又通过引擎贴到场景上,这样就使物体有了光照的感觉。

alpha blend工作原理

Alpha Blend 实现透明效果,不过只能针对某块区域进行alpha操作,透明度可设。

alpha blend 用于做半透明效果。Color = (源颜色 源系数) OP ( 目标颜色 目标系数);其中OP(混合方式)有加,减,反减,取最小,取最大

Unity的Shader中,Blend SrcAlpha OneMinusSrcAlpha这句话是什么意思

作用就是Alpha混合。公式:最终颜色 = 源颜色 x 源透明值 + 目标颜色 x(1 - 源透明值)

alpha test在何时使用?能达到什么效果

Alpha Test ,中文就是透明度测试。简而言之就是V&F shader中最后fragment函数输出的该点颜色值(即上一讲frag的输出half4)的alpha值与固定值进行比较。AlphaTest语句通常于Pass{}中的起始位置。Alpha Test产生的效果也很极端,要么完全透明,即看不到,要么完全不透明。

Vertex Shader是什么,怎么计算

顶点着色器是一段执行在GPU上的程序,用来取代fixed pipeline中的transformation和lighting,Vertex Shader主要操作顶点。
Vertex Shader对输入顶点完成了从local space到homogeneous space(齐次空间)的变换过程,homogeneous space即projection space的下一个space。在这其间共有world transformation, view transformation和projection transformation及lighting几个过程。

写出光照计算中的diffuse(漫反射)的计算公式

diffuse = Kd x colorLight x max(N*L,0);
Kd 漫反射系数、colorLight 光的颜色、N 单位法线向量、L 由点指向光源的单位向量、其中N与L点乘,如果结果小于等于0,则漫反射为0。

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
答:漫反射光(diffuse)计算公式为:Idiffuse = Dintensity*Dcolor*N.L ; (Dintensity表示漫反射强度,Dcolor表示漫反射光颜色,N为该点的法向量,L为光源向量)

其他,3D渲染中,物体表面的光照计算公式为:

I = 环境光(Iambient) + 漫反射光(Idiffuse) + 镜面高光(Ispecular);

其中,环境光(ambient)计算公式为:

Iambient= Aintensity* Acolor; (Aintensity表示环境光强度,Acolor表示环境光颜色)

漫反射光(diffuse)计算公式为:

Idiffuse = Dintensity*Dcolor*N.L ; (Dintensity表示漫反射强度,Dcolor表示漫反射光颜色,N为该点的法向量,L为光源向量)

镜面光照(specular)计算公式为:

Ispecular = Sintensity*Scolor*(R.V)n; (Sintensity表示镜面光照强度,Scolor表示镜面光颜色,R为光的反射向量,V为观察者向量)

综上所得:整个光照公式为:

I = Aintensity* Acolor+ Dintensity*Dcolor*N.L + Sintensity*Scolor*(R.V)n ;

将一些值合并,并使用白色作为光照颜色,则上述公式可简化为:

I = A + D*N.L + (R.V)n

MeshRender中material和sharedmaterial的区别(?

修改sharedMaterial将改变所有物体使用这个材质的外观,并且也改变储存在工程里的材质设置。不推荐修改由sharedMaterial返回的材质。如果你想修改渲染器的材质,使用material替代。

简述水面倒影的渲染原理

原理就是对水面的贴图纹理进行扰动,以产生波光玲玲的效果。用shader可以通过GPU在像素级别作扰动,效果细腻,需要的顶点少,速度快

什么叫动态合批?跟静态合批有什么区别

如果动态物体共用着相同的材质,那么Unity会自动对这些物体进行批处理。动态批处理操作是自动完成的,并不需要你进行额外的操作。
区别:动态批处理一切都是自动的,不需要做任何操作,而且物体是可以移动的,但是限制很多。静态批处理:自由度很高,限制很少,缺点可能会占用更多的内存,而且经过静态批处理后的所有物体都不可以再移动了。

两种阴影判断的方法、工作原理。

本影:景物表面上那些没有被光源直接照射的区域(全黑的轮廓分明的区域)。
半影:景物表面上那些被某些特定光源直接照射但并非被所有特定光源直接照射的区域(半明半暗区域)
工作原理:从光源处向物体的所有可见面投射光线,将这些面投影到场景中得到投影面,再将这些投影面与场景中的其他平面求交得出阴影多边形,保存这些阴影多边形信息,然后再按视点位置对场景进行相应处理得到所要求的视图(利用空间换时间,每次只需依据视点位置进行一次阴影计算即可,省去了一次消隐过程)

Unity提供了几种光源,分别是什么

四种。
平行光:Directional Light
点光源:Point Light
聚光灯:Spot Light
区域光源:Area Light

实时点光源的优缺点是什么

可以有cookies – 带有 alpha通道的立方图(Cubemap )纹理。点光源是最耗费资源的。

GPU的工作原理

简而言之,GPU的图形(处理)流水线完成如下的工作:(并不一定是按照如下顺序) 顶点处理:这阶段GPU读取描述3D图形外观的顶点数据并根据顶点数据确定3D图形的形状及位置关系,建立起3D图形的骨架。在支持DX8和DX9规格的GPU中,这些工作由硬件实现的Vertex Shader(定点着色器)完成。 光栅化计算:显示器实际显示的图像是由像素组成的,我们需要将上面生成的图形上的点和线通过一定的算法转换到相应的像素点。把一个矢量图形转换为一系列像素点的过程就称为光栅化。例如,一条数学表示的斜线段,最终被转化成阶梯状的连续像素点。 纹理帖图:顶点单元生成的多边形只构成了3D物体的轮廓,而纹理映射(texture mapping)工作完成对多变形表面的帖图,通俗的说,就是将多边形的表面贴上相应的图片,从而生成“真实”的图形。TMU(Texture mapping unit)即是用来完成此项工作。 像素处理:这阶段(在对每个像素进行光栅化处理期间)GPU完成对像素的计算和处理,从而确定每个像素的最终属性。在支持DX8和DX9规格的GPU中,这些工作由硬件实现的Pixel Shader(像素着色器)完成。 最终输出:由ROP(光栅化引擎)最终完成像素的输出,1帧渲染完毕后,被送到显存帧缓冲区。
总结:GPU的工作通俗的来说就是完成3D图形的生成,将图形映射到相应的像素点上,对每个像素进行计算确定最终颜色并完成输出。

图形学

光照模型有哪些

3维模型组成

Mesh

Mesh下面有哪些字段

如向将文理贴在模型上

图片向格式有那些

U3D问题总结(四) 物理相关

作者 Anqi Zhao
2019年10月30日 07:00

射线检测碰撞物的原理是

射线是3D世界中一个点向一个方向发射的一条无终点的线,在发射轨迹中与其他物体发生碰撞时,它将停止发射 。

Unity3d中的碰撞器和触发器的区别?

碰撞器是触发器的载体,触发器是碰撞器的属性
Is Trigger=false,碰撞器根据物理引擎引发碰撞,产生碰撞的效果
此时调用OnCollisionEnter/Stay/Exit函数
Is Trigger=true,碰撞器被物理引擎所忽略,没有碰撞效果
此时调用OnTriggerEnter/Stay/Exit函数

发生碰撞的必要条件

两个物体都必须带有碰撞器(Collider)
其中至少有一个物体带有刚体(Rigidbody)或者角色控制器(CharacController)
必须是运动的物体带有Rigidbody脚本才能检测到碰撞

物体发生碰撞的过程有几个阶段

1.OnCollisionEnter
2.OnCollisionStay
3.OnCollisionExit

当一个细小的高速物体撞向另一个较大的物体时,会出现什么情况?如何避免?

穿透(碰撞检测失败)
1)增大细小物体的碰撞体(不建议这样做)
(2)使用射线检测,检测他们之间的距离
(3)FixedUpdate频率修改,可以physics time减小(同样不建议)
(4)改变物体的速度(废话)
(5)将检测方式改为连续检测,rigifdbody.collisionDetectionMode =CollisionDetectionMode.Continuous;
或者是动态连续检测(CollisionDetectionMode.ContinuousDynamic)
(6)代码限制,加大计算量 提前计算好下一个位置

Unity3d物理引擎中,有几种施加力的方式(?

rigidbody.AddForce/AddForceAtPosition,都在rigidbody系列函数中

CharacterController和Rigdibody的区别

Rigidbody:刚体组件、用于模拟真实的物理效果、可以受到重力和其他力的作用、这个力可以直接施加、也可以来自其他刚体的碰撞
CharacterController:角色控制组件,它自带一个胶囊控制器,能够受到重力的影响。移动时使用自身的Move()、SimpleMove()方法

Rigidbody具有完全真实物理的特性,Unity中物理系统最基本的一个组件,包含了常用的物理特性,而CharacterController可以说是受限的的Rigidbody,具有一定的物理效果但不是完全真实的,是Unity为了使开发者能方便的开发第一人称视角的游戏而封装的一个组件

什么叫做链条关节(?

Hinge Joint,可以模拟两个物体间用一根链条连接在一起的情况,能保持两个物体在一个固定距离内部相互移动而不产生作用力,但是达到固定距离后就会产生拉力。

U3D问题总结(三) Unity基础

作者 Anqi Zhao
2019年10月30日 07:00

Unity基础

Unity和Android与iOS如何交互

Unity可以导出安卓和IOS的工程。导出的工程有Unity封装的方法,可以通过发消息的方式跟Unity进行交互。其中,C#不能与OC直接进行交互,需要用C++去写。

Unity3D支持的作为脚本的语言

C#、JS、Boo

Unity中用过哪些插件?具体功能

NGUI,制作2D界面

Helpshift,帮助与提示

KTPlay,游戏论坛

UniClipboard,粘贴板

Unity中常用的插件(Δ)

Unity引擎使用的是左手坐标系还是右手坐标系(A)

A.左手坐标系 B.右手坐标系
C.可以通过ProjectSetting切换右手坐标系 D.可以通过Reference切换左手坐标系

什么是导航网格(NavMesh)(B)

A.一种用于描述相机轨迹的网格 B.一种用于实现自动寻址的网格
C.一种被优化过的物体网格 D.一种用于物理碰撞的网格

生命周期

OnEnable、Awake、Start运行时发生的顺序,哪些可以在同一周期中重复发生

Awake->OnEnable->Start
OnEnable可以在同一周期中重复发生

生命周期顺序

Awake——>OnEnable–>Start——>Update——>FixedUpdate——>LateUpdate——>OnGUI——>OnDisable——>OnDestroy

Awake() 脚本唤醒,系统执行的第一个方法,用于脚本初始化,只执行一次。

Start()在Awake之后、Update之前执行,只执行一次。

Update()用于逻辑正常更新,每帧由系统自动调用一次。

FixedUpdate()固定更新。

LateUpdate()推迟更新,每帧调用,在Update之后调用。

OnGUI() 每帧可能会被绘制多次,每次对应于一个 GUI event

OnDestroy()当前脚本销毁时调用。

12.以下关于 MonoBehaviour.OnGUI()的描述错误的是(D)

A.如果 MonoBehaviour 没有被启用,则OnGUI函数不会被调用
B.用于绘制和处理 GUI events
C.每帧可能会被绘制多次,每次对应于一个 GUI event
D.每帧被调用一次

Addcomponent后哪个生命周期函数会被调用?

对于AddComponent添加的脚本,其Awake,Start,OnEnable是在Add的当前帧被调用的,其中Awake,OnEnable与AddComponent处于同一调用链上,Start会在当前帧稍晚一些的时候被调用。Update则是根据Add调用时机决定何时调用:如果Add是在当前帧的Update前调用,那么新脚本的Update也会在当前帧被调用,否则会被延迟到下一帧调用。
https://blog.csdn.net/qq_32821435/article/details/94760815

物理更新一般放在哪个系统函数里

FixedUpdate。update跟当前平台的帧数有关,而FixedUpdate是真实时间

Update和FixedUpdate的区别?

Update是在每次渲染新的一帧的时候会调用,FixedUpdate,是在固定的时间间隔执行,不受游戏帧率的影响。FixedUpdate的时间间隔可以在项目设置中更改,Edit->ProjectSetting->time 找到Fixedtimestep。

移动相机动作在哪个函数里,为什么在这个函数里

LateUpdate,是在所有的update结束后才调用,比较适合用于命令脚本的执行。官网上例子是摄像机的跟随,都是所有的update操作完才进行摄像机的跟进,不然就有可能出现摄像机已经推进了,但是视角里还未有角色的空帧出现。

关于 MonoBehaviour.LateUpdate 函数描述错误的是:(B)

A.当 MonoBehaviour 类被启用后,每帧调用一次
B.常被用于处理 Rigidbody 的更新
C.在所有 Update 函数执行后才能被调用
D.常被用于实现跟随相机效果,且目标物体的位置已经在 Update 函数中被更新

应该放在FixedUpdate

如何让已经存在的GameObject在LoadLevel后不被卸载掉

1
2
3
4
void Awake()
{
DontDestroyOnLoad(transform.gameObject);
}

脚本及编辑器

命名空间

unityEngne

9.在哪个面板中可以修改物体的空间属性,如位置,朝向,大小等(B)

A.Project B.Inspector C.Hierarchy D.Toolbar

如何为一个Asset 资源设定一个Label,从而能够方便准确的搜索到?(D)

A.在Project窗口中选中一个Asset,右键->Create->Label
B.在Project窗口中选中一个Asset,右键->Add Label
C.在Project窗口中选中一个Asset,在Inspector窗口中点击添加Label的图标
D.在Project窗口中选中一个Asset,在Inspector窗口中点击按钮“Add Label”

5.Application.loadLevel命令为(A)

A.加载关卡 B.异步加载关卡 C.加载动作 D.加载动画

将图片的TextureType选项分别选为“Texture”和“Sprite”有什么区别

Sprite作为UI精灵使用,Texture作用模型贴图使用。Sprite需要2的整次幂,打包图片省资源

为什么dynamic font在unicode环境下优于static font(?

Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。
使用动态字体时,Unity将不会预先生成一个与所有字体的字符纹理。当需要支持亚洲语言或者较大的字体的时候,若使用正常纹理,则字体的纹理将非常大。

OnBecameVisible及OnBecameInvisible的发生时机,以及这一对回调函数的意义

当物体是否可见切换之时。可以用于只需要在物体可见时才进行的计算。

采用Input.mousePosition 来获取鼠标在屏幕上的位置,以下表达正确的是(C)

A.左上角为原点(0,0),右下角为(Screen.Width, Screen.Height)
B.左下角为原点(0,0),右下角为(Screen.Height, Screen.Width)
C.左下角为原点(0,0),右上角为(Screen.Width, Screen.Height)
D.左上角为原点(0,0),右下角为(Screen.Height, Screen.Width)

物体自身旋转使用的函数?

Transform.Rotate()

物体绕某点旋转使用函数叫什么?

transform.RotateAround()

以下选项中,将游戏对象绕Z轴逆时针旋转90度(C)

A.transform.rotation = Quaternion.Euler(0,0,90)
B.transform.rotation = Quaternion.Angle(0,0,90)
C.transform.Rotate(new Vector3(0,0,90))
D.transform.Rotate(new Vector3(90,0,0))

游戏对象B是游戏对象A的子物体,游戏对象A经过了旋转,请写出游戏B围绕自身的Y轴进行旋转的脚本语句,以及游戏对象B围绕世界坐标的Y轴旋转的脚本语句

绕世界坐标旋转:transform.Rotate (transform.upspeedTime.deltatime);
绕自身Y轴旋转:transform.Rotate (Vector.upspeedTime.deltatime);

U3D中用于记录节点空间几何信息的组件名称,及其父类名称

Transform 父类是 Component

关于Vector3 的API,以下说法正确的是(BC)

A.Vector3.normalize 可以获取一个三维向量的法线向量
B.Vector3.magnitude 可以获取一个三维向量的长度
C.Vector3.forward 与 Vector3(0,0,1)是一样的意思
D.Vector3.Dot(向量A,向量B)是用来计算向量A与向量B的叉乘

以下哪个函数在游戏进入新场景后会被马上调用(B)

A.MonoBehaviour.OnSceneWastLoaded()
B.MonoBehaviour.OnSceneEnter()
C.MonoBehaviour.OnLevelEnter()
D.MonoBehaviour.OnLevelWastLoaded()

14.在Unity引擎中,Collider所指的是什么(D)

A.collider 是Unity引擎中所支持的一种资源,可用作存储网格信息
B.Collider 是Unity引擎中内置的一种组件,可用对网格进行渲染
C.Collider 是Unity引擎中所支持的一种资源,可用作游戏对象的坐标转换
D.Collider 是Unity引擎中内置的一种组件,可用作游戏对象之间的碰撞检测

下列选项中,关于Transform组件的Scale参数描述正确的是(A)

A.Transform组件的Scale参数不会影响ParticleSystem产生粒子的大小
B.Transform组件的Scale参数不会影响GUITexture的大小
C.添加Collider组件后的GameoObject,其 Collider 组件的尺寸不受Transform组件的Scale参数影响
D.添加Rigidbody组件后的物体,大小将不再受Transform组件中 Scale 参数的影响

如何销毁一个UnityEngine.Object及其子类

Destroy()方法

DestroyImmediate和Destroy的区别

DestroyImmeditate 销毁对象的时候,会立即释放资源。Destroy只是从该场景销毁,但是还在内存当中。

在编辑场景时将GameObject设置为Static有何作用

设置游戏对象为Static时,这些部分被静态物体挡住而不可见时,将会剔除(或禁用)网格对象。因此,在你的场景中的所有不会动的物体都应该标记为Static。

如何通过脚本来删除其自身对应的Gameobject(A)

A.Destroy(gameObject) B.this.Destroy()
C.Destroy(this) D.其他三项都可以

某个GameObject有一个名为MyScript的脚本,该脚本中有一个名为DoSomething 的函数,则如何在该Gameobject的另外一个脚本中调用该函数?(A)

A.GetComponent().DoSomething()
B.GetComponent
C.GetComponent().Call(“DoSomething”)
D.GetComponent

CompareTag比直接用gameObject.tag要好

简述一下对象池

对象池就存放需要被反复调用资源的一个空间,当一个对象回大量生成的时候如果每次都销毁创建会很费时间,通过对象池把暂时不用的对象放到一个池中(也就是一个集合),当下次要重新生成这个对象的时候先去池中查找一下是否有可用的对象,如果有的话就直接拿出来使用,不需要再创建,如果池中没有可用的对象,才需要重新创建,利用空间换时间来达到游戏的高速运行效果,在FPS游戏中要常被大量复制的对象包括子弹,敌人,粒子等

对象池使用什么数据结构构建

频繁创建GameObject会降低程序性能为什么?怎么解决?

频繁创建游戏对象,会增加游戏的Drawcall数,降低帧率,GPU会一直在渲染绘制。可以通过对象池来管理对象:当需要创建一个游戏对象时,先去对象池中查找一下对象池中是否存在没有被正在使用的对象,如果有的话直接使用这个对象,并把它标记为正在使用,没有话就创建一个,并把它添加到池中,然后标记为使用中。一个游戏对象使用完毕的时候,不要销毁掉,把它放在池中,标记为未使用。

如何在Unity中创建地形系统?(D)

A.Terrain->Create Terrain B.Component->Create Terrain
C.Asset->Create Terrain D.Windows->Create Terrain

资源相关

当删除Unity工程Assets目录下地meta文件时会导致什么?为什么?

会导致在场景中游戏对象看不到,或者报错,材质找不到资源。多人协作的时候会导致资源的重复产生。因为每个资源文件都对应一个.meta文件,这个.meta文件中的guid就是唯一标识这个资源的。材质就是通过这个guid来记录自己使用了那些资源,而且同一个资源的guid会因为不同的电脑而不同,所以当你上传了丢失了.meta文件的资源的时候,到了别人的机器上就会重新产生guid,那个这个资源就相当于垃圾了。

meta文件的作用

prefab的作用

  1. 在游戏运行时实例化。Prefab相当于一个模板,对已有的素材、脚本和参数做一个基础的配置,便于以后的修改
  2. Prefab打包的内容简化了导出操作,便于团队协同

下列叙述中有关 Prefab 说法错误的是哪一项(B)

A.Prefab 是一种资源类型 B.Prefab 是一种可以反复使用的游戏对象
C.Prefab 可以多次在场景进行实例 D.当一个 Prefab 添加到场景中时,也就是创建了它的一个实例

资源加载方式

1.Resources
2.AssetBundle
3.AssetDatabase

资源数据库 (AssetDatabase)

资源数据库 (AssetDatabase) 是允许您访问工程中的资源的 API。此外,其提供方法供您查找和加载资源,还可创建、删除和修改资源。Unity 编辑器 (Editor) 在内部使用资源数据库 (AssetDatabase) 追踪资源文件,并维护资源和引用资源的对象之间的关联。Unity 需要追踪工程文件夹发生的所有变化,如需访问或修改资源数据,您应始终使用资源数据库 (AssetDatabase) API,而非文件系统。 资源数据库 (AssetDatabase) 接口仅适用于编辑器,不可用于内置播放器。和所有其他编辑器类一样,其只适用于置于编辑器 (Editor) 文件夹中的脚本(只在主要的资源 (Assets) 文件夹中创建名为“编辑器”的文件夹(不存在该文件夹的情况下))。

什么是AssetBundle?谈谈对AssetBundle内存分配情况的理解

可以把多个游戏对象或资源二进制文件封装到AssetBundle中,提供封装与解包的方法使用很方便。

加载资源三个步骤:

  1. www/LoadFromFile/LoadFromMemory等接口加载AssetBundle本身
  2. AssetBundle.LoadAsset()等接口从AssetBundle中加载资源
  3. 对于GameObject类资源,需要通过GameObject.Instantiate()创建Clone

黑色区域:www类本身占用内存,还保留了一份对WebStream数据的引用。使用www = null或www.dispose()释放。前者等待GC,后者立即释放。释放后WebStream引用计数会减一。

橙色区域:WebStream数据,数据真正的存储区域。AssetBundle被加载进来后,这部分内存就被分配了。包含三个内容:1、压缩后的AssetBundle本身。2、解压后的资源。3、一个解压缓冲区。www或AssetBundle对象都只是有一个结构指向了WebStream数据,从而对外部提供操作真正资源数据的方法。当WebStream数据引用为0时,系统会自动释放。为了不频繁的开辟和销毁解压Buffer,绿色Decompression解压缓冲区Unity会至少保留一份。

粉色区域:AssetBundle对象,引用WebStream数据部分,提供从WebStream数据中加载资源的接口。AssetBundle.Unload(bool unloadAllLoadedObjects)释放资源。AssetBundle.Unload(false)释放AssetBundle对象本身,可能引起WebStream释放,导致无法通过接口或依赖关系从该AssetBundle加载资源,但已加载资源可以正常使用。AssetBundle(true)不仅释放WebStream部分,所有被加载出来的资源将被释放。

红色部分:通过Instantiate()创建的GameObject所包含的资源。这些资源根据类型与AssetBundle原始资源(WebStream资源部分)有不同关系。如Texture、shader资源,通常只是使用,不会做出改动,所以仅仅是引用关系;每个GameObject是特殊的,所以是完全复制一份;Mesh和Material,则是引用+复制的关系。

动态加载资源的方式 区别

1.Resources.Load();
2.AssetBundle

1.通过Resources模块,调用它的load函数:可以直接load并返回某个类型的Object,前提是要把这个资源放在Resource命名的文件夹下,Unity不关有没有场景引用,都会将其全部打入到安装包中。
2.通过bundle的形式:即将资源打成 asset bundle 放在服务器或本地磁盘,然后使用WWW模块get 下来,然后从这个bundle中load某个object。

以下关于WWW.LoadFromCacheOrDownload描述正确的是(C)

A.可被用于将 Text Assets 自动缓存到本地磁盘
B.可被用于将 Resource 自动缓存到本地磁盘
C.可被用于将 Asset Bundles 自动缓存到本地磁盘
D.可被用于将任意格式的Unity资源文件自动缓存到本地磁盘

如何安全地在不同工程间安全地迁移asset数据(?

  1. 将Assets目录和Library目录一起迁移
  2. 导出包,export Package
  3. 用unity自带的assets Server功能 或者meta功能

AssetBundle包加载流程

图集打包怎么分类

1.按业务功能的预制,寻找依赖,收集所有预制引用的图片,
2.如果有多个预制使用了同一张图片,我们就把它扔到common文件夹
3.让图集尽量紧凑,没有太多空白,尽量让图集处于2的n次方大小

为什么Unity3d中会发生在组件上出现数据丢失的情况(?

组件上绑定的物体对象被删除了

UI与Camera

UGUI的Canvas的作用

Canvas画布是承载所有UI元素的区域。所有的UI元素都必须是Canvas的子对象。如果场景中没有画布,那么我们创建任何一个UI元素,都会自动创建画布,并且将新元素置于其下。

创建Canvas:GameObject->UI->Canvas

如何实现UI界面的层级

Unity3d实现2d游戏,有几种方式

  1. 使用自身的GUI
  2. 把摄像机的Projection(投影)值调整为Orthographic(正交投影),不考虑z轴
  3. 使用2d的ui插件:2DToolKit、NGUI等

为何大家都在移动设备上寻求U3D原生GUI的替代方案

不美观,OnGUI很耗费时间,效率不高,使用不方便

如何在不同分辨率下保持UI的一致性(?

NGUI很好的解决了这一点,屏幕分辨率的自适应性,原理就是计算出屏幕的宽高比跟原来的预设的屏幕分辨率求出一个对比值,然后修改摄像机的size。UGUI通过锚点和中心点和分辨率也解决这个问题

ngui和ugui的区别

简述NGUI中Grid和Table的作用

对Grid和Table下的子物体进行排序和定位

请简述NGUI中Panel和Anchor的作用

  1. 只要提供一个half-pixel偏移量,它可以让一个控件的位置在Windows系统上精确的显示出来(只有这个Anchor的子控件会受到影响)
  2. 如果挂载到一个对象上,那么他可以将这个对象依附到屏幕的角落或者边缘
    3.UIPanel用来收集和管理它下面所有widget的组件。通过widget的geometry创建实际的draw call。没有panel所有东西都不能够被渲染出来,你可以把UIPanel当做Renderer

UGUI中Image和RawImage的区别

Imgae比RawImage更消耗性能
Image只能使用Sprite属性的图片,但是RawImage什么样的都可以使用
Image适合放一些有操作的图片,裁剪平铺旋转什么的,针对Image Type属性
RawImage就放单独展示的图片就可以,性能会比Image好很多

在场景中放置多个Camera并同时处于活动状态会发生什么(?

游戏界面可以看到很多摄像机的混合。可以用depth(深度),Layer(层)+ Culling Mask,enable = false/true来控制

照相机的Clipping Planes的作用是什么?调整Near、Fare两个值时,应该注意什么

剪裁平面 。从相机到开始渲染和停止渲染之间的距离。

将Camera组件的ClearFlags选项选成Depth only是什么意思?有何用处

如果把摄像机的ClearFlags勾选为Deapth Only,那么摄像机就会只渲染看得见的对象,把背景会完全透明,这种情况一般用在两个摄像机以上的场景中

在 Unity 中的场景中创建 Camera 时,默认情况下除了带有Transform、Camera、GUILayer、Flare Layer 组件之外,还带有以下哪种组件(C)

A.Mouse Look B.FPS Input Controller C.Audio Listener D.Character Motor

以下哪组摄像机中 Normalized View Port Rect 的数值设置可以使摄像机显示的画面位于1280*720分辨率的屏幕画面右上角(D)

A.X=640,Y=360,W=640,H=360 B.X=640,Y=0,W=640,H=360
C.X=0,Y=0,W=0.5,H=0.5 D.X=0.5,Y=0.5,W=0.5,H=0.5

多媒体

如果将一个声音剪辑文件从Project 视图拖动到 Inspector 视图或者 Scene 视图中的游戏对象上,该游戏对象会自动添加以下哪种组件(C)

A.Audio Listener B.Audio Clip C.Audio Source D.Audio Reverb Zone

以下哪一个选项不属于Unity引擎所支持的视频格式文件(D)

A.后缀为mov的文件 B.后缀为mpg的文件
C.后缀为avi的文件 D.后缀为swf的文件

请描述游戏动画有哪几种

主要有关节动画、骨骼动画、单一网格模型动画(关键帧动画)。
关节动画:把角色分成若干独立部分,一个部分对应一个网格模型,部分的动画连接成一个整体的动画,角色比较灵活,Quake2中使用这种动画;
骨骼动画,广泛应用的动画方式,集成了以上两个方式的优点,骨骼按角色特点组成一定的层次结构,有关节相连,可做相对运动,皮肤作为单一网格蒙在骨骼之外,决定角色的外观;
单一网格模型动画由一个完整的网格模型构成,在动画序列的关键帧里记录各个顶点的原位置及其改变量,然后插值运算实现动画效果,角色动画较真实。

下列选项中有关Animator的说法错误的是?(D)

A.Animator是Unity引擎中内置的组件
B.任何一个具有动画状态机功能的GameObject都需要一个Anim组件
C.它主要用于角色行为的设置,包括StateMachine、混合树BlendTrees以及同通过脚本控制的事件
D.Animator同Animation组件的用法是相同的

Animator.CrossFade 命令作用是:(B)

A.动画放大 B.动画转换 C.Update() D.OnMouseButton()

Animation和Animator的区别

Animation需要通过代码手动控制动画的播放和迁移。而Animator拥有有动画状态机,可以通过动画状态机来设置动画之间的状态,并且可以为单个动画设置脚本代码来控制事件。

其他

Unity3d提供了一个用于保存和读取数据的类(PlayerPrefs),请列出保存和读取整形数据的函数

PlayerPrefs.SetInt(string, int) PlayerPrefs.GetInt(string)

Unity3D是否支持写成多线程程序?如果支持的话需要注意什么

仅能从主线程中访问Unity3D的组件,对象和Unity3D系统调用
支持:如果同时你要处理很多事情或者与Unity的对象互动小可以用thread,否则使用coroutine。
注意:C#中有lock这个关键字,以确保只有一个线程可以在特定时间内访问特定的对象

Unity3D的协程和C#线程之间的区别是什么?

多线程程序同时运行多个线程 ,而在任一指定时刻只有一个协程在运行,并且这个正在运行的协同程序只在必要时才被挂起。除主线程之外的线程无法访问Unity3D的对象、组件、方法。
Unity3d没有多线程的概念,不过unity也给我们提供了StartCoroutine(协同程序)和LoadLevelAsync(异步加载关卡)后台加载场景的方法。 StartCoroutine为什么叫协同程序呢,所谓协同,就是当你在StartCoroutine的函数体里处理一段代码时,利用yield语句等待执行结果,这期间不影响主程序的继续执行,可以协同工作。

❌
❌