今天整理邮件时,偶然发现了我在二十多年前,上小学时写的一个类似《小蜜蜂》的游戏,大概是小学五年级或者六年级,用的 QBasic。

因为我没有配置 QBasic 的环境,所以也不确定还能不能用。但看这个程序,真的恍如隔世。

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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
DECLARE SUB Initialize ()
DECLARE SUB DrawPlayer ()
DECLARE SUB MovePlayer (direction AS INTEGER)
DECLARE SUB Shoot ()
DECLARE SUB CheckInput ()
DECLARE SUB InitializeEnemies ()
DECLARE SUB MoveEnemies ()
DECLARE SUB EnemyShoot ()
DECLARE SUB MoveEnemyBullets ()
DECLARE SUB DrawEnemies ()
DECLARE SUB CheckCollisions ()
DECLARE SUB GameOver ()
DECLARE SUB DrawGame ()

CONST screenWidth = 80
CONST screenHeight = 25
CONST playerChar = "A"
CONST bulletChar = "^"
CONST enemyBulletChar = "*"
CONST enemyChar = "V"
CONST maxEnemies = 5
CONST maxEnemyBullets = 10

TYPE Enemy
x AS INTEGER
y AS INTEGER
dx AS INTEGER
active AS BOOLEAN
END TYPE

TYPE Bullet
x AS INTEGER
y AS INTEGER
active AS BOOLEAN
END TYPE

DIM SHARED playerX AS INTEGER, playerY AS INTEGER
DIM SHARED playerBullet AS Bullet
DIM SHARED enemies(maxEnemies) AS Enemy
DIM SHARED enemyBullets(maxEnemyBullets) AS Bullet
DIM SHARED score AS INTEGER
DIM SHARED gameRunning AS BOOLEAN

RANDOMIZE TIMER
Initialize

DO WHILE gameRunning AND NOT INKEY$ = CHR$(27)
CALL CheckInput
CALL MoveEnemies
CALL EnemyShoot
CALL MoveEnemyBullets
CALL CheckCollisions
CALL DrawGame
SLEEP 1
LOOP

IF NOT gameRunning THEN
CALL GameOver
END IF

SUB Initialize
playerX = screenWidth / 2
playerY = screenHeight - 1
playerBullet.active = FALSE
score = 0
gameRunning = TRUE
CALL InitializeEnemies
END SUB

SUB InitializeEnemies
FOR i = 1 TO maxEnemies
enemies(i).x = INT(RND * (screenWidth - 2)) + 2
enemies(i).y = INT(RND * 5) + 1
enemies(i).dx = IIF(RND > 0.5, 1, -1)
enemies(i).active = TRUE
NEXT i
END SUB

SUB DrawGame
CLS
LOCATE playerY, playerX
PRINT playerChar;
IF playerBullet.active THEN
LOCATE playerBullet.y, playerBullet.x
PRINT bulletChar;
END IF
FOR i = 1 TO maxEnemies
IF enemies(i).active THEN
LOCATE enemies(i).y, enemies(i).x
PRINT enemyChar;
END IF
NEXT i
FOR i = 1 TO maxEnemyBullets
IF enemyBullets(i).active THEN
LOCATE enemyBullets(i).y, enemyBullets(i).x
PRINT enemyBulletChar;
END IF
NEXT i
LOCATE 1, 1
PRINT "Score: "; score
END SUB

SUB MovePlayer (direction AS INTEGER)
IF direction = -1 AND playerX > 1 THEN
playerX = playerX - 1
ELSEIF direction = 1 AND playerX < screenWidth THEN
playerX = playerX + 1
END IF
END SUB

SUB Shoot
IF NOT playerBullet.active THEN
playerBullet.x = playerX
playerBullet.y = playerY - 1
playerBullet.active = TRUE
END IF
END SUB

SUB CheckInput
DIM key AS STRING
key = INKEY$
SELECT CASE key
CASE CHR$(75) ' left
CALL MovePlayer(-1)
CASE CHR$(77) ' right
CALL MovePlayer(1)
CASE " " ' space
CALL Shoot
END SELECT
END SUB

SUB MoveEnemies
FOR i = 1 TO maxEnemies
IF enemies(i).active THEN
enemies(i).x = enemies(i).x + enemies(i).dx
IF enemies(i).x <= 1 OR enemies(i).x >= screenWidth THEN
enemies(i).dx = -enemies(i).dx
enemies(i).y = enemies(i).y + 1
END IF
IF enemies(i).y >= screenHeight THEN
enemies(i).y = 1
END IF
END IF
NEXT i
END SUB

SUB EnemyShoot
FOR i = 1 TO maxEnemies
IF enemies(i).active AND INT(RND * 10) < 2 THEN
FOR j = 1 TO maxEnemyBullets
IF NOT enemyBullets(j).active THEN
enemyBullets(j).x = enemies(i).x
enemyBullets(j).y = enemies(i).y + 1
enemyBullets(j).active = TRUE
EXIT FOR
END IF
NEXT j
END IF
NEXT i
END SUB

SUB MoveEnemyBullets
FOR i = 1 TO maxEnemyBullets
IF enemyBullets(i).active THEN
enemyBullets(i).y = enemyBullets(i).y + 1
IF enemyBullets(i).y > screenHeight THEN
enemyBullets(i).active = FALSE
END IF
END IF
NEXT i
END SUB

SUB CheckCollisions
IF playerBullet.active THEN
FOR i = 1 TO maxEnemies
IF enemies(i).active THEN
IF playerBullet.x = enemies(i).x AND playerBullet.y = enemies(i).y THEN
playerBullet.active = FALSE
enemies(i).active = FALSE
score = score + 10
EXIT FOR
END IF
END IF
NEXT i
END IF

FOR i = 1 TO maxEnemyBullets
IF enemyBullets(i).active THEN
IF enemyBullets(i).x = playerX AND enemyBullets(i).y = playerY THEN
gameRunning = FALSE
END IF
END IF
NEXT i
END SUB

SUB GameOver
CLS
LOCATE screenHeight / 2, (screenWidth / 2) - 5
PRINT "Game Over! Score: "; score
SLEEP 5
END SUB

最近有个强烈的心得体会,独立游戏开发者必须要同时做策划和程序两件事,很容易出现的情况是,策划推进10%,然后程序再推进10%,之后重复这个过程,看似两条线在并行。

但是,这种开发模式有严重的问题,很容易导致返工。因为两边都可能考虑的不够完善,可能你策划到后期才发现程序给你挖了一个巨大的坑,之后就要来回返工。

最科学的开发方式应该是先铆钉策划或者程序,把任何一项的进度推进到90%以上,再去碰另外一件事。

我有个书稿需要解决《A》(B)结构里,把B斜体的问题,一个一个改实在太麻烦了,所以写了个 Python 解决这个问题。

python-docx 库其实很粗糙,改不了所有的地方,甚至还会把字体弄乱了。但是实测大概可以改掉70%,已经极大程度减小工作量了 …… 至于字体和结构乱了重排一下就好。

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
from docx import Document
import re

def italicize_pattern_in_docx(docx_path):
doc = Document(docx_path)
pattern = r'《([^》]*)》(([^)]*))'

for paragraph in doc.paragraphs:
matches = re.finditer(pattern, paragraph.text)
for match in matches:
start, end = match.start(2), match.end(2)
inline = paragraph.runs
before = paragraph.text[:start]
italic_text = paragraph.text[start:end]
after = paragraph.text[end:]

paragraph.clear()
if before:
paragraph.add_run(before)
italic_run = paragraph.add_run(italic_text)
italic_run.italic = True
if after:
paragraph.add_run(after)

doc.save('修改_' + docx_path)

docx_path = '文档.docx'
italicize_pattern_in_docx(docx_path)

AudioSource.isPlaying 只要程序失焦就会生效,同时也有可能莫名其妙触发,我这几天发现无规则触发了好多次。比较确定的是禁用 AudioSource 组件后,再启用,之后 isPlaying 的判断就会出问题,至于什么原理就不清楚了。

比较安全的方法是通过将当前时间与开始播放时的时间戳相减,你可以得到音频播放了多长时间。然后,将这个播放时长与音频剪辑的总时长(AudioClip.length)比较。如果播放时长小于音频总时长,那么可以认为音频仍在播放中。

代码大概如下:

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
public class AudioSourceTimeChecker : MonoBehaviour
{
public AudioSource audioSource;
private float startTime = -1;
private float clipLength = 0;

public void PlayAudio()
{
if(audioSource.clip != null)
{
audioSource.Play();
startTime = Time.time;
clipLength = audioSource.clip.length;
}
}

public bool IsAudioPlaying()
{
if(startTime < 0) return false; // 没有播放音频

float elapsedTime = Time.time - startTime;
bool isPlaying = elapsedTime < clipLength;
if (!isPlaying)
{
// 重置startTime为-1,表示当前没有音频在播放
startTime = -1;
}
return isPlaying;
}
}

我有个全是 index 的 pdf 文件,要把里面 index 内容提取出来,然后存成 Excel 文件,同时还要删除原有的页码。

规则是:每行第一个逗号前的所有内容都保留,第一个逗号后的内容如果是英文单词也保留,如果是数字就删除,之后所有逗号后面的内容都删除。

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
import pdfplumber
import re
import pandas as pd

# 正则表达式匹配每行第一个逗号前的内容,以及可能的英文单词
pattern = re.compile(r"([^,]+),\s*(\b[a-zA-Z]+\b)?")

index_items = []

with pdfplumber.open("输入.pdf") as pdf:
# 遍历PDF的每一页
for page in pdf.pages:
# 提取当前页的文本
text = page.extract_text()
# 根据换行符分割文本以处理每一行
lines = text.split('\n')
for line in lines:
# 使用正则表达式找到每行的匹配项
match = pattern.search(line)
if match:
# 如果有第二个分组(英文单词),则保留这个单词
if match.group(2):
item = match.group(1).strip() + ', ' + match.group(2).strip()
else:
item = match.group(1).strip()
index_items.append(item)

df = pd.DataFrame(index_items, columns=["Index Item"])

df.to_excel("输出.xlsx", index=False)

今天有个奇怪的尝试,打算实现一下让场景里一张图片直接转换成为黑白的效果,首先想到的是使用 Shader。

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
Shader "Custom/GrayscaleShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;

v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
float grayscale = dot(col.rgb, float3(0.299, 0.587, 0.114));
return fixed4(grayscale, grayscale, grayscale, col.a);
}
ENDCG
}
}
}

创建一个新的Material,并将新创建的Shader赋给这个Material。在你的场景中,找到想要转换为黑白的SpriteRenderer,并将这个新创建的Material赋给它们。

但是操作起来太麻烦了,于是尝试了一种纯粹使用代码完成的思路:

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
using UnityEngine;

public class ConvertSpriteToBlackAndWhite : MonoBehaviour
{
void Start()
{
SpriteRenderer[] allSprites = FindObjectsOfType<SpriteRenderer>();
foreach (SpriteRenderer spriteRenderer in allSprites)
{
ConvertToBlackAndWhite(spriteRenderer);
}
}

void ConvertToBlackAndWhite(SpriteRenderer spriteRenderer)
{
if (spriteRenderer.sprite == null) return;

Texture2D originalTexture = spriteRenderer.sprite.texture;
Texture2D grayscaleTexture = new Texture2D(originalTexture.width, originalTexture.height);
Color[] originalPixels = originalTexture.GetPixels();
Color[] grayscalePixels = new Color[originalPixels.Length];

for (int i = 0; i < originalPixels.Length; i++)
{
Color originalColor = originalPixels[i];
float grayscaleValue = originalColor.grayscale;
grayscalePixels[i] = new Color(grayscaleValue, grayscaleValue, grayscaleValue, originalColor.a);
}

grayscaleTexture.SetPixels(grayscalePixels);
grayscaleTexture.Apply();

spriteRenderer.sprite = Sprite.Create(grayscaleTexture, spriteRenderer.sprite.rect, new Vector2(0.5f, 0.5f));
}
}

原理是,复制Sprite的原始纹理,然后对这个复制品的每个像素应用灰度转换。当然这种方法的效率非常低。

之后又改进了一下代码,可以实现渐变为黑白的效果:

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
using System.Collections;
using UnityEngine;

public class GradualBlackAndWhite : MonoBehaviour
{
public float transitionDuration = 2f; // 渐变持续时间,以秒为单位

void Start()
{
StartCoroutine(ApplyBlackAndWhiteGradually());
}

IEnumerator ApplyBlackAndWhiteGradually()
{
SpriteRenderer[] allSprites = FindObjectsOfType<SpriteRenderer>();
float time = 0;

while (time < transitionDuration)
{
foreach (SpriteRenderer spriteRenderer in allSprites)
{
ConvertToBlackAndWhite(spriteRenderer, time / transitionDuration);
}

time += Time.deltaTime;
yield return null;
}

// 确保最终是完全黑白的
foreach (SpriteRenderer spriteRenderer in allSprites)
{
ConvertToBlackAndWhite(spriteRenderer, 1);
}
}

void ConvertToBlackAndWhite(SpriteRenderer spriteRenderer, float lerpFactor)
{
if (spriteRenderer.sprite == null) return;

Texture2D originalTexture = spriteRenderer.sprite.texture;
Texture2D grayscaleTexture = new Texture2D(originalTexture.width, originalTexture.height);
Color[] originalPixels = originalTexture.GetPixels();
Color[] grayscalePixels = new Color[originalPixels.Length];

for (int i = 0; i < originalPixels.Length; i++)
{
Color originalColor = originalPixels[i];
float grayscaleValue = originalColor.grayscale;
Color grayscaleColor = new Color(grayscaleValue, grayscaleValue, grayscaleValue, originalColor.a);
grayscalePixels[i] = Color.Lerp(originalColor, grayscaleColor, lerpFactor);
}

grayscaleTexture.SetPixels(grayscalePixels);
grayscaleTexture.Apply();

spriteRenderer.sprite = Sprite.Create(grayscaleTexture, spriteRenderer.sprite.rect, new Vector2(0.5f, 0.5f));
}
}

最后测试了一下,转换为黑白的效果有点没必要,虽然代码写了,但最后没用 ……

今天想实现一下Unity中的相机抖动,又不想因为这么小的一个功能去找个插件,所以写了个很简单的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.Collections;
using UnityEngine;

public class CameraShake : MonoBehaviour
{
public IEnumerator Shake(float duration, float magnitude)
{
Vector3 originalPosition = transform.position;
float elapsed = 0.0f;

while (elapsed < duration)
{
float x = Random.Range(-1f, 1f) * magnitude;
float y = Random.Range(-1f, 1f) * magnitude;

transform.position = new Vector3(originalPosition.x + x, originalPosition.y + y, originalPosition.z);
elapsed += Time.deltaTime;

yield return null;
}

transform.position = originalPosition;
}
}

把这段代码挂载在Camera上,之后在其他的脚本里使用下面方法调用就可以:

1
2
3
4
5
6

public CameraShake cameraShake;

……

StartCoroutine(cameraShake.Shake(0.5f, 0.5f)); // 震动0.5秒,幅度0.5

Char 是一个我的摸鱼游戏。

有一个冷门的需求,我需要把一个字体的 ttf 文件里的每个英文字母都导出成为一个单独的图片。依然是使用 Python 写了个程序解决:

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
from PIL import Image, ImageDraw, ImageFont
from fontTools.ttLib import TTFont
import os
import string

def extract_and_save_characters_from_ttf(ttf_path, output_folder):
# 加载字体文件
font = TTFont(ttf_path)
ttf = ImageFont.truetype(ttf_path, 40)

# 创建输出文件夹,如果它不存在的话
if not os.path.exists(output_folder):
os.makedirs(output_folder)

# 定义一个函数来清理文件名
def clean_filename(char, char_code):
valid_chars = f"-_.() {string.ascii_letters}{string.digits}"
cleaned = ''.join(c for c in char if c in valid_chars)
# 在文件名中加入字符的Unicode编码
return f"{cleaned}_{char_code}"

# 遍历字体文件中的每个字符
for table in font['cmap'].tables:
for char_code in table.cmap:
char = chr(char_code)
image = Image.new('RGBA', (50, 50), (255, 255, 255, 0)) # 创建带有透明背景的图片
draw = ImageDraw.Draw(image)
draw.text((10, 5), char, font=ttf, fill=(255, 255, 255, 255)) # 使用白色绘制文字

# 清理字符名称,用于文件名
cleaned_char = clean_filename(char, char_code)

# 保存字符的图片文件
image.save(os.path.join(output_folder, f'{cleaned_char}.png'))

extract_and_save_characters_from_ttf(r'文件地址.ttf', 'output_folder')

Python 有一个名为 markdown 的库就可以处理这个问题,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import markdown
import sys

def convert_md_to_html(md_file_path, html_file_path):
# 读取Markdown文件
with open(md_file_path, 'r', encoding='utf-8') as md_file:
md_content = md_file.read()

# 使用markdown库将Markdown转换为HTML
html_content = markdown.markdown(md_content)

# 写入HTML文件
with open(html_file_path, 'w', encoding='utf-8') as html_file:
html_file.write(html_content)

print(f"Markdown file {md_file_path} has been converted to HTML file {html_file_path}")

if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python md_to_html.py [markdown_file_path] [html_file_path]")
else:
md_file_path = sys.argv[1]
html_file_path = sys.argv[2]
convert_md_to_html(md_file_path, html_file_path)

然后用下面代码执行就可以:

1
python md_to_html.py example.md example.html

其实绝大多数的 Markdown 文件,都可以用正则表达式手写完成:

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
import re

def md_to_html(md_content):
"""将Markdown文本转换为HTML。"""

# 转换标题
md_content = re.sub(r'### (.*)', r'<h3>\1</h3>', md_content)
md_content = re.sub(r'## (.*)', r'<h2>\1</h2>', md_content)
md_content = re.sub(r'# (.*)', r'<h1>\1</h1>', md_content)

# 转换粗体和斜体
md_content = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', md_content)
md_content = re.sub(r'\*(.*?)\*', r'<em>\1</em>', md_content)
md_content = re.sub(r'_(.*?)_', r'<em>\1</em>', md_content)

# 转换无序列表
md_content = re.sub(r'\n\* (.*)', r'\n<li>\1</li>', md_content)
md_content = re.sub(r'(<li>.*?</li>)', r'<ul>\1</ul>', md_content, flags=re.DOTALL)

# 包裹在基本的HTML结构中
html_content = f"""<!DOCTYPE html>
<html>
<head>
<title>Markdown Converted Document</title>
<meta charset="utf-8">
</head>
<body>
{md_content}
</body>
</html>"""

return html_content

def convert_md_to_html(md_file_path, html_file_path):
with open(md_file_path, 'r', encoding='utf-8') as md_file:
md_content = md_file.read()

html_content = md_to_html(md_content)

with open(html_file_path, 'w', encoding='utf-8') as html_file:
html_file.write(html_content)

print(f"Markdown file {md_file_path} has been converted to HTML file {html_file_path}")

if __name__ == "__main__":
import sys
if len(sys.argv) != 3:
print("Usage: python md_to_html.py [markdown_file_path] [html_file_path]")
else:
md_file_path = sys.argv[1]
html_file_path = sys.argv[2]
convert_md_to_html(md_file_path, html_file_path)

今天学到了一个看似很常见,但是我以前没写过的东西,就是case穿透。我以前一直以为 C# 不能这么写(好像还是在书上之类的地方看到的),今天尝试了一下发现居然没问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
switch (number)
{
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
// 执行0到5对应的操作
Debug.Log("数字在0到5之间");
break;
default:
// 其他情况的操作
Debug.Log("其他数字");
break;
}

也就是一直遇到 break 以后,case 才会停止执行。