unity对话框打印机特效

分享一下我的对话框打印机特效

上下文

uGUI 已经变成弃用了,我使用的最近 unity 6 的 UI Document。

小镇Town 场景有一个 TownUI 的 UIDocument, TownUI里面有一个类似 Prefab 的Dialog 的UIDocuement, Dialog 里面是一个VisualElement 作为背景, 里面是ScrollView, 然后里面是Label, 你会看到我的代码里也是这么查询的。

TownHandler 变成static 是方便在变得场景中随时调用

showStr 变成public 是为了支持可以在 inspector 那边随时更改

实现

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
using UnityEngine;
using UnityEngine.UIElements;
using System.Collections;

public class TownHandler : MonoBehaviour
{

public static TownHandler instance { get; private set; }



public float displayTime = 4.0f;
private VisualElement m_NonPlayerDialogue;
private float m_TimerDisplay;

public string showStr; //需要打出来的字
private Label label; //打字展示的文本
private ScrollView scrollView;




// Awake is called when the script instance is being loaded (in this situation, when the game scene loads)
private void Awake()
{
instance = this;
}


// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{

UIDocument uiDocument = GetComponent<UIDocument>();
//m_Healthbar = uiDocument.rootVisualElement.Q<VisualElement>("HealthBar");
//SetHealthValue(1.0f);


m_NonPlayerDialogue = uiDocument.rootVisualElement.Q<VisualElement>("Dialog");
m_NonPlayerDialogue.style.display = DisplayStyle.None;
m_TimerDisplay = -1.0f;


scrollView = m_NonPlayerDialogue.Q<ScrollView>();
label = scrollView.Q<Label>(); // 使用 Label 的名称

Debug.LogWarning("label is " + label);
}

// Update is called once per frame
void Update()
{
//if (m_TimerDisplay > 0)
//{
// m_TimerDisplay -= Time.deltaTime;
// if (m_TimerDisplay < 0)
// {
// m_NonPlayerDialogue.style.display = DisplayStyle.None;
// }


//}
}

public void DisplayDialogue()
{
Debug.Log("DisplayDialogue");
m_NonPlayerDialogue.style.display = DisplayStyle.Flex;
m_TimerDisplay = displayTime;

StartCoroutine(PrintWord());

}

IEnumerator PrintWord()
{

foreach (char letter in showStr)
{
label.text += letter; // 添加一个字符
yield return new WaitForSeconds(0.1f); // 模拟打印速度,调整时间来控制速度


scrollView.scrollOffset = new Vector2(0, label.resolvedStyle.height); // 设置滚动位置到最底部
}



}

}

重装机兵重构

背景

看到 《大灾变:黑暗之日》 竟然还在维护,突然很感动,想到了重装机兵在国内应该也有一定的受众。于是,再次开始游戏吧。

过程

Day1

  1. 安装 Unity Hub
  2. 按照这个视频一步步学习简单的2d物理引擎 https://learn.unity.com/tutorial/playground-get-started-on-your-first-game?language=en#

Day2

按照这个教程开始2d游戏的高阶基础 https://learn.unity.com/tutorial/set-up-tilemap-collision?uv=2022.3&courseId=64774201edbc2a1638d25d18&projectId=6477424bedbc2a1473e5fce4# ,

人物可以移动,可以和sprite碰撞, 也可以和tilemap 碰撞

  1. 认识 Main window
  2. 认识 Context Menu
  3. 认识 Hierarchy Menu
  4. 认识 Project Menu
  5. 认识 Inspector Menu
  6. 认识 物理引擎 Rigidbody 2D
  7. 认识 碰撞器 Box Collider 2D
  8. 认识 Grid + TileMap
  9. 认识 Palette (tileSet)
  10. 认识 Rect Tool
  11. 认识 prefab 的作用

Day3

继续昨天的教程, 人物移动可以扣血回血, 并在血条上体现, enemy 的巡逻
  1. 认识 Box Collider 2D 的is Trigger 会导致可以穿透,并有事件
  2. 认识 Box Collider 2D 的 auto Tiling
  3. 认识 UI Builder(UI Document)
  4. 认识 VisualElement
  5. PlayerController 的碰撞脚本和静态的 UIHandler 进行血量交互
  6. 认识 Animator
  7. 认识 Animation
  8. 认识 Blend Tree

Day4

  1. Blend Tree 增加 transtion
  2. 认识 Layers (用于发射子弹)
  3. 认识 Edit > Project Settings… > Physics 2D -> the Layer Collision Matrix
  4. 认识 Raycasting
  5. 遇到了子弹射出去不移动的问题
  6. 遇到了子弹从enemy身上穿过去的问题
  7. 实现了对话弹出ui
  8. 认识cinimachine (摄像头碰撞)

Day5

  1. 认识 Audio Source
  2. 认识 Audio Source 的 Spatial Blend
  3. 认识 Effects -> Particle System
  4. 认识 粒子的 Radius 和 angle
  5. 如何手搓动画, 主要是关键帧
  6. Editor -> Project Setting -> Player 设置分辨率,版本
  7. File -> Building Profile -> 设置scene -> Build And Run

Day6

  1. 使用 UIDocument 增加游戏入口 Main , 监听 Enter 利用 SceneManager 切换到另一个场景
  2. unity store 购买免费的 DOTWEEN 插件, 然后实现场景切换时候的fade动画

Day7

  1. 复习实现 NPC 的弹窗对话
  2. 文字的打字机特效
  3. 自动滚动到ScrollView 的最下面

Day8

  1. 修复刚进入场景是 animator 迟缓(关闭Transitions 的 has Exit time)
  2. 在Moving(Blend Tree) 的基础上增加 Idle 的 Blend Tree
  3. 把6张真人的照片 去除背景, 然后处理成 像素图
  4. 学习战斗系统 英文教程, 在b站上看的盗版的
  5. 认识 Rigidbody 2D 的 Body Type: Dynamic / Kinematic / Static
  6. 认识 Rigidbody 2D 的 Collistion Detection: Discrete / Continuous

2025春节

Day9

  1. 参考b站上的中文教程
  2. 认识 Physics2D.OverlapCircle, 实现仇恨范围, 敌人自动追踪
  3. 认识 Cinemachine vs Camera Folllow

Day10

  1. 认识PlayerInput(Script) 和 InputAction 的区别
  2. 认识跨脚本调用,除了使用static 方法, 还可以使用 gameObject.BroadcastMessage()
  3. 在animation里面插入关键帧, 实现动画打击(假设是4帧, 就是1,2 帧 disable, 3帧enable, 4帧disable)

Day11

  1. 利用Collider 的 isTriger实现击退效果, 被伤害动画
  2. 实现死亡效果(死亡的动画插入event, 然后Destory掉 gameObject)
  3. 认识 TextMeshPro, 实现漂浮文字
  4. 实现漂浮文字的暴击效果, 动画效果

至此,动画相关除了持久化都已经测试结果,开始地图篇

docker 容器重启追踪

背景

无意中看到 docker ps 显示容器重启了, 虽然服务正常,还是准备查询一下

步骤

确认到服务重启

找到上一个停止的容器的日志

查看日志

发现容器是正常停止的, 基本排除服务器重启

确认机器没有重启

查看docker日志

可以发现是node 的状态从new 变成了down

查看系统日志

监控到软件oom 了

1
journalctl -e


可以在aliyun 的监控看到当时cpu有增加,但是只有75%

结论

prometheus 的容器oom导致的docker node 节点的down, 但还不知道为什么会导致我的traefik 的容器重启,暂时的解决方式是给 prometheus 加上资源限制

openwrt上面克隆git仓库

问题与背景

为了进一步升级openwrt的能力范畴,很多服务又没有docker镜像, 所以只能在wrt上面跑代码,这样就涉及到了git克隆仓库,但是openwrt默认的ssh client是dropbear 的实现方式,这是一个针对小内存设备的特别版本,使用 git clone git@github.com:amanoooo/amanosblog.git 就会报错

现象

1
2
3
4
5
6
7
root@amanoswrt:~/amano# git clone git@github.com:amanoooo/amanosblog.git
Cloning into 'amanosblog'...
Connection closed by 20.205.243.166 port 22
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

解决

  1. 第一步安装openssh-client

  2. 检查可以发现ssh已经链接到openssh 了

    1
    2
    3
    4
    root@amanospi:~# which ssh
    /usr/bin/ssh
    root@amanospi:~# ls -al /usr/bin/ssh
    lrwxr-xr-x 1 root root 24 Jan 6 16:57 /usr/bin/ssh -> /usr/libexec/ssh-openssh
  3. 更新 ~/.ssh/config 文件, 增加下面的配置

    1
    2
    3
    Host github.com
    Hostname ssh.github.com
    Port 443

tspl打印图片

背景

上一篇文章聊了 nodejs 打印标签, 现在需要增加难度,把图片打印上去

解决方案

参考这篇文章, 作者已经实现了, 但是他的写法已经不支持最新的版本了, 我来翻新一下

新代码

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
const usb = require('usb');
const { Jimp, intToRGBA } = require('jimp');
console.log('Jimp is ', Jimp);
const fs = require('fs')
let iconv = require('iconv-lite');

const json = JSON.parse(fs.readFileSync('logo.json', { encoding: 'utf-8' }))
const imgWidthInBytes = json[0].length;
const imgHeightInDots = json.length;


const cmds = [
'SIZE 48 mm, 10 mm',
'CLS',
'TEXT 1,1,"TSS24.BF2",0,1,1,"你好"',
'PRINT 1',
'END'
]
// const cmds = [
// 'SIZE 48 mm,25 mm',
// 'CLS',
// 'TEXT 10,10,"4",0,1,1,"HackerNoon"',
// 'TEXT 10,40,"4",0,1,1,"amano"',
// 'BARCODE 10,100,"128",90,1,0,2,2,"altospos.com"',
// 'PRINT 1',
// 'END',
// ];



async function getImageData(path) {
const img = await Jimp.read(path)
const bitmap = img.bitmap
const widthInBytes = Math.ceil(bitmap.width / 8);
const data = new Array(bitmap.height);
for (let y = 0; y < bitmap.height; y++) {
const row = new Array(widthInBytes);
for (let b = 0; b < widthInBytes; b++) {
let byte = 0;
let mask = 128;
for (let x = b * 8; x < (b + 1) * 8; x++) {
const color = intToRGBA(img.getPixelColor(x, y));
if (color.a < 65) byte = byte ^ mask;
mask = mask >> 1;
}
row[b] = byte;
}
data[y] = row;
}
return data;
}



function print(cmds) {
let device = usb.findByIds(1137, 85)

console.log('cmds is ', cmds);

device.open();
device.interfaces[0].claim();
const outEndpoint = device.interfaces[0].endpoints.find(e => e.direction === 'out');
outEndpoint.transferType = 2;

const processedCmds = cmds.map(cmd => {
if (cmd.startsWith('BITMAP')) {
return Buffer.from(cmd);
}
else if (cmd.startsWith('RAW ')) {
// 去掉 'HEX ' 前缀并将十六进制字符串转换为 Buffer
const jsonData = JSON.parse(cmd.slice(4).trim());
console.log('jsonData is ', jsonData);
return Buffer.from(jsonData.flat());
} else {
// 普通命令按原样处理
return iconv.encode(cmd + '\r\n', 'gbk');
}
});

outEndpoint.transfer(Buffer.concat(processedCmds), (err) => {
if (err) {
console.error('Transfer error:', err);
}
device.close();
})
}


const main = async () => {

const list = usb.getDeviceList()
list.forEach(device => {
console.log(`Device: ${device.deviceDescriptor.idVendor}:${device.deviceDescriptor.idProduct}`);
});



print(cmds)

};



async function generateImg() {

const res = await getImageData('logo-niumag.png')
console.log('res is ', res);
fs.writeFileSync('./logo.json', JSON.stringify(res))
}

// generateImg()
main()

老代码

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
const usb = require('usb');
const Jimp = require('jimp');

function getImageData(path, cb) {
Jimp.read(path, (err, img) => {
const widthInBytes = Math.ceil(img.getWidth() / 8);
const data = new Array(img.getHeight());
for (let y = 0; y < img.getHeight(); y++) {
const row = new Array(widthInBytes);
for (let b = 0; b < widthInBytes; b++) {
let byte = 0;
let mask = 128;
for (let x = b*8; x < (b+1)*8; x++) {
const color = Jimp.intToRGBA(img.getPixelColor(x, y));
if (color.a < 65) byte = byte ^ mask; // empty dot (1)
mask = mask >> 1;
}
row[b] = byte;
}
data[y] = row;
}
cb(data);
});
}

function print(buffer) {
// you can get all available devices with usb.getDeviceList()
let device = usb.findByIds(/*vid*/8137, /*pid*/8214);
device.open();
device.interfaces[0].claim();
const outEndpoint = device.interfaces[0].endpoints.find(e => e.direction === 'out');
outEndpoint.transferType = 2;
outEndpoint.transfer(buffer, (err) => {
device.close();
});
}

getImageData('hn-logo.png', (data) => {
const widthInBytes = data[0].length;
const heightInDots = data.length;

const buffer = Buffer.concat([
Buffer.from('SIZE 48 mm,25 mm\r\n'),
Buffer.from('CLS\r\n'),
Buffer.from(`BITMAP 10,20,${widthInBytes},${heightInDots},0,`),
Buffer.from(data.flat()),
Buffer.from('BARCODE 10,100,"128",50,1,0,2,2,"altospos.com"\r\n'),
Buffer.from('PRINT 1\r\n'),
Buffer.from('END\r\n'),
]);

print(buffer);
});

参考图片

1.
hn-logo.png

2.
logo-niumag.png

2024windows_powershell_乱码

背景

  1. 最近一些桌面端的项目启动的输出怎么乱码
  2. git 的一些操作也出现乱码

问题测试

可以复制这些到一个文件里面, 然后直接在powershell 里面执行

1
2
3
@echo off
echo test chinese character view 测试中文字符显示
pause

解决方案

方案1

  1. windows上我通过 chcp 65001 && npm run start 可以解决

方案2

  1. 参考这个 文章, 第一步查看powershell 配置位置
1
$PROFILE
  1. 在该配置文件中添加如下配置:
1
$OutputEncoding = [console]::InputEncoding = [console]::OutputEncoding = New-Object System.Text.UTF8Encoding
  1. 验证
1
./test.bat

2024docker使用代理

背景

和上一篇文章类似,墙内世界自己推送了image之后, 服务器(我的环境是centos)上拉取也得使用代理。
但是最近很多伙伴反馈只简单的更新环境变量没有效果,通过文档上面更新docker daemon.json 也没有效果

解决

还是参考官方文档, 使用 systemd unit file

  1. Create a systemd drop-in directory for the docker service:
1
sudo mkdir -p /etc/systemd/system/docker.service.d
  1. Create a file named /etc/systemd/system/docker.service.d/http-proxy.conf that adds the HTTP_PROXY environment variable:
1
2
3
4
5
[Service]
Environment="HTTP_PROXY=http://127.0.0.1:7890"
Environment="HTTPS_PROXY=http://127.0.0.1:7890"
Environment="NO_PROXY=localhost,127.0.0.1,docker-registry.example.com,.corp"

  1. Flush changes and restart Docker
1
2
sudo systemctl daemon-reload
sudo systemctl restart docker
  1. Verify that the configuration has been loaded and matches the changes you made, for example:
1
2
3
sudo systemctl show --property=Environment docker

Environment=HTTP_PROXY=http://127.0.0.1:7890 HTTPS_PROXY=http://127.0.0.1:7890 NO_PROXY=localhost,127.0.0.1,docker-registry.example.com,.corp

注意

我偷懒只写了http_proxy, 结果发现没有效果, 请一定不能省略https_proxy

2024安装clash

背景

https://github.com/Dreamacro/clash 因为晒车牌被抓住了,很多脚本都失效了,这里提供一个解决方案

方式

  1. 下载
1
2
3
wget https://archlinux.org/packages/extra/x86_64/clash/download
mv download clash.tar
tar -xvf clash.tar
  1. 运行
1
2
3
clash ./usr/bin/clash
# INFO[0000] Can't find MMDB, start download
# FATA[0000] Initial configuration directory error: can't initial MMDB: can't download MMDB: Get "https://cdn.jsdelivr.net/gh/Dreamacro/maxmind-geoip@release/Country.mmdb": read tcp 172.24.35.37:39168->8.7.198.46:443: read: connection reset by peer
  1. [可选]补充

如果第一步 archlinux 下载不了, 可以手动下载安装包, 然后 scp 到服务器上
如果第二步 Country.mmdb 下载不了, 可以手动下载, 然后 scp 到服务器的 ~/.config/clash 文件夹

  1. 更新配置

默认的配置文件是 ~/.config/clash 这里, 参考我的

1
2
3
4
➜  clash pwd
/root/.config/clash
➜ clash ls
cache.db config.yaml config.yaml.bak Country.mmdb
  1. 类unix命令行使用
1
export https_proxy=http://127.0.0.1:7890;export http_proxy=http://127.0.0.1:7890;export all_proxy=socks5://127.0.0.1:7890

containerd常见命令

背景

自docker hub以及各种源被墙之后, 阿里云的ack服务就拉取不到docker镜像啦, ack服务底层又是使用containerd, 所以手动使用docker 命令拉取镜像也不可以,需要使用 containerd 对应的命令, 比如 crictl.

但是 crictl 不支持 http_proxy, 所以使用 ctr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 使用代理
export https_proxy=http://127.0.0.1:7890;export http_proxy=http://127.0.0.1:7890;export all_proxy=socks5://127.0.0.1:7890

# 查看命名空间
ctr namespaces list

# 查看镜像列表
ctr -n k8s.io images list

# 拉取获取推送
ctr -n k8s.io images pull -u username:pass registry.niumag.com/namespace/image:snap
ctr -n k8s.io images push -u username:pass registry.niumag.com/namespace/image:snap

# 删除
ctr -n k8s.io images rm registry.niumag.com/namespace/image:snap

nodejs 打印标签

背景

公司有需求, 打印一些小票

  1. 直接搜索 node-printer 或者 node-thermal-printer 都太古老了。
  2. windows 自带的打印命令 不好调试
  3. 这里分享下使用 TSPL指令条码打印机 的一些解决方案

TSPL

示例

1
2
3
4
5
SIZE 60 mm,40 mm\r\n
GAP 2 mm\r\n
CLS\r\n
TEXT 50,50,\"4\",0,1,1,\"DEMO FOR TEXT\"\r\n
PRINT 1,1\r\n

文档

https://open.jolimark.com/files/tspl.pdf

步骤

  1. 找到设备端口
1
2
3
4
5
6
const usb = require('usb');

const list = usb.getDeviceList()
list.forEach(device => {
console.log(`Device: ${device.deviceDescriptor.idVendor}:${device.deviceDescriptor.idProduct}`);
});
  1. 连接设备
1
2
3
4
5
6
7
8
9
10
let device = usb.findByIds(1137, 85)
console.log('device is ', device);

device.open();
device.interfaces[0].claim();
const outEndpoint = device.interfaces[0].endpoints.find(e => e.direction === 'out');
outEndpoint.transferType = 2;
outEndpoint.transfer(Buffer.from(cmds.join('\r\n')), (err) => {
device.close();
});
  1. [选填] windows 需要刷新设备的驱动,可以参考 usb 的文档

    On Windows, if you get LIBUSB_ERROR_NOT_SUPPORTED when attempting to open your device, it’s possible your device doesn’t have a WinUSB driver for libusb to use.

    You can install one using Zadig or another approach is to use the UsbDK Backend of libusb by immediately calling usb.useUsbDkBackend().

    Note that you cannot use multiple drivers on Windows as they get exclusive access to the device. So if you want to switch between drivers (e.g. using a printer with this software or the system), you will need to uninstall/install drivers as required.

    For further info, check How to use libusb on Windows in the libusb’s wiki.

刷后的效果 如图

完整代码

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
const usb = require('usb');

const cmds = [
'SIZE 48 mm,25 mm',
'CLS',
'TEXT 10,10,"4",0,1,1,"HackerNoon"',
'TEXT 10,40,"4",0,1,1,"amano"',
'BARCODE 10,100,"128",90,1,0,2,2,"altospos.com"',
'PRINT 1',
'END',
];

// you can get all available devices with usb.getDeviceList()
const list = usb.getDeviceList()
console.log('list is ', list);
list.forEach(device => {
console.log(`Device: ${device.deviceDescriptor.idVendor}:${device.deviceDescriptor.idProduct}`);
});
// let device = list[0]
let device = usb.findByIds(1137, 85)
console.log('device is ', device);

device.open();
device.interfaces[0].claim();
const outEndpoint = device.interfaces[0].endpoints.find(e => e.direction === 'out');
outEndpoint.transferType = 2;
outEndpoint.transfer(Buffer.from(cmds.join('\r\n')), (err) => {
device.close();
});