这几天一直在纠结一件事,要不要把 Resources 全换成 AssetBundle,因为众所周知,Resources 的效率很差,甚至 Unity 官方都不太推荐使用 Resources。纠结了一下,最终决定还是使用 Resources ,一方面是懒得改代码了 …… 另一方面其实稍微想了一下,也觉得没必要,绝大多数的 Resource 滥用都是因为使用了大量材质包,但我使用主要是音频问题,并且我算了一下,好像这些音频文件打包之后,体积也没想象中的大,本来 Unity 也不会把音频也不会全加载到内存里。只要注意不要 Resources 的文件结构搞的太复杂就好。

当然,这件事很可能过两天又变了。因为我已经变了前几天的点子了,本来打算直接用 Excel 做表导出到游戏里,但是尝试一下就放弃了,游戏里有好多只使用了一次的解谜内容,为了这些内容去定制表没必要,还是手动一点一点来。

今天的开发日志完全是为了展示学好 Python 对开发效率提高多大。

因为 Resource 下面的文件结构太复杂,需要生成大量空的文件夹,所以写了两行 bat 来做这件事:

1
2
3
4
5
@echo off
FOR /L %%G IN (0,1,100) DO (
mkdir %%G
)
echo 目录创建成功。

之后我这里的音频文件文件名结构是数字+空格+文件名,需要复制到对应数字的文件夹里。本来打算手动一个一个复制,复制2个以后就放弃了,然后打算用 bat 再写一段,结果不知道怎么用 bat 写 …… 所以又用 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
import os
import shutil

# 在这里设置你的目录
source_dir = '目录A'
target_dir = '目录B'

# 读取源目录中的所有文件
for file in os.listdir(source_dir):
if file.endswith('.mp3'):
# 从文件名中提取数字
number = file.split(' ')[0]
if number.isdigit():
# 构建源文件和目标文件的路径
source_file = os.path.join(source_dir, file)
target_subdir = os.path.join(target_dir, number)

# 检查目标子目录是否存在
if os.path.exists(target_subdir) and os.path.isdir(target_subdir):
target_file = os.path.join(target_subdir, file)

# 复制文件
shutil.copy2(source_file, target_file)
print(f"已将 '{file}' 复制到 '{target_subdir}'")
else:
print(f"目标目录 '{target_subdir}' 不存在。跳过 '{file}'。")
else:
print(f"文件 '{file}' 开头没有数字。跳过。")
else:
print(f"文件 '{file}' 不是MP3格式。跳过。")

print("完成!")

写完以后觉得用两个程序实现这件事太蠢了,于是用 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
37
38
39
40
41
42
import os
import shutil

# 第一步:创建数字命名的文件夹
# 设置目标目录
source_dir = '目录A'

# 创建名为0到100的目录
for i in range(101):
os.makedirs(os.path.join(target_dir, str(i)), exist_ok=True)

print("目录创建成功。")

# 第二步:复制文件
# 设置源目录
target_dir = '目录B'

# 读取源目录中的所有文件
for file in os.listdir(source_dir):
if file.endswith('.mp3'):
# 从文件名中提取数字
number = file.split(' ')[0]
if number.isdigit():
# 构建源文件和目标文件的路径
source_file = os.path.join(source_dir, file)
target_subdir = os.path.join(target_dir, number)

# 检查目标子目录是否存在
if os.path.exists(target_subdir) and os.path.isdir(target_subdir):
target_file = os.path.join(target_subdir, file)

# 复制文件
shutil.copy2(source_file, target_file)
print(f"已将 '{file}' 复制到 '{target_subdir}'")
else:
print(f"目标目录 '{target_subdir}' 不存在。跳过 '{file}'。")
else:
print(f"文件 '{file}' 开头没有数字。跳过。")
else:
print(f"文件 '{file}' 不是MP3格式。跳过。")

print("复制完成!")

在写完以上的内容后,突然觉得还是自动化的流程更有效率,于是决定重新整理开发文档的格式,之后直接用程序配表。

但是我之前的文档是 Word 格式的,我要转换成 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
31
32
33
34
35
36
37
38
39
40
41
42
43
from docx import Document
import pandas as pd

# 加载 Word 文档
file_path = 'word路径' # 替换为您的文件路径
doc = Document(file_path)

# 处理表格并返回 DataFrame 的函数
def process_table(table):
data = []
for row in table.rows:
row_data = []
for cell in row.cells:
row_data.append(cell.text)
data.append(row_data)
return pd.DataFrame(data)

# 将 Word 文档中的每个表格处理为 DataFrame
tables = [process_table(table) for table in doc.tables]

# 设置列名并清洗、分割选项列的函数
def set_columns_clean_split_table(table):
# 设置列名
table.columns = ['文件名', '内容', '选项']

# 从 '选项' 列中提取选项文本和数字
table['选项文本'] = table['选项'].str.extract(r'^(.*?)[,,]')
table['数字'] = table['选项'].str.extract(r'([0-9]+)$')

# 删除原始的 '选项' 列
table.drop(columns=['选项'], inplace=True)

return table

# 应用函数处理所有表格
processed_tables = [set_columns_clean_split_table(table) for table in tables]

# 将所有表格合并为一个 DataFrame
final_merged_table = pd.concat(processed_tables, ignore_index=True)

# 将最终合并的表格保存为 Excel 文件
final_excel_path = 'excel路径' # 替换为您的保存路径
final_merged_table.to_excel(final_excel_path, index=False)

Python 真的是强大的生产力工具。

开始每日更新开发日志,基本玩法已经确定,第一轮的测试音频也已经完成,大体的美术风格也确定了,昨天换了一张 Midjourney 生成的背景图相当适合游戏氛围。

现在有两个大的问题:一是故事框架要细化,现在的故事太粗糙了,需要细化为完整的文案;二是谜题要整理好,因为游戏玩法太特殊了,所以必须有特殊的谜题,才能让玩家不觉得乏味。

本来说今天就开始上量开发,但是 Unity 疯狂崩溃,研究了一下发现是 Plastic SCM 的问题,会提示用户名或密码错误,之后会卡死整个 Unity。但是密码我从来没改过,我在客户端里登录也没提示出错,解决方案是重新安装了一遍 Platstic SCM,但是看晚上有人说把对应文件夹删了应该也可以。

求求你 Unity 能不能修修 Bug。

我十几年前有过一个习惯,会把自己每天电脑屏幕的内容截图生成一个视频,这样每天看视频就知道自己做了什么。

这几天突然又想到了这件事,但是当年开发的程序是 Delphi 写的,已经在 Windows 11 上跑不起来了,于是用 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
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
import pyautogui
import time
import datetime
import cv2
import os
from moviepy.editor import VideoFileClip, concatenate_videoclips

def create_video(image_folder, video_name):
images = [img for img in os.listdir(image_folder) if img.endswith(".png")]
frame = cv2.imread(os.path.join(image_folder, images[0]))
height, width, layers = frame.shape

video = cv2.VideoWriter(video_name, cv2.VideoWriter_fourcc(*'mp4v'), 24, (width, height)) # 24是帧数

for image in images:
video.write(cv2.imread(os.path.join(image_folder, image)))
os.remove(os.path.join(image_folder, image)) # 删除图片

cv2.destroyAllWindows()
video.release()

def find_videos_of_the_day(directory, date):
videos = []
for file in os.listdir(directory):
if file.endswith(".mp4") and date in file:
videos.append(file)
videos.sort(key=lambda x: os.path.getmtime(os.path.join(directory, x)))
return videos

def merge_videos(videos, output_filename):
clips = [VideoFileClip(video) for video in videos]
final_clip = concatenate_videoclips(clips)
final_clip.write_videofile(output_filename, codec="libx264")

try:
while True:
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
screenshot_filename = f"screenshot_{timestamp}.png"

screenshot = pyautogui.screenshot()
screenshot.save(screenshot_filename)
print(f"截图已保存: {screenshot_filename}")

time.sleep(10)

except KeyboardInterrupt:
today_date = datetime.datetime.now().strftime("%Y%m%d")
existing_videos = find_videos_of_the_day('.', today_date)

choice = input("你要生成视频吗?(是/否): ")
if choice.lower() == '是':
print("正在生成视频...")

# 创建新视频文件
new_video_filename = f"temp_video_{today_date}.mp4"
create_video('.', new_video_filename)

# 合并视频
final_videos = existing_videos + [new_video_filename]
output_filename = f"video_{today_date}.mp4"
merge_videos(final_videos, output_filename)
os.remove(new_video_filename) # 删除临时视频文件

print(f"视频{output_filename}生成完毕,所有截图已删除。")

如果缺什么直接用 pip install 安装一下对应的就可以。

如果需要生成为 exe 文件,可以用 PyInstaller ,用法如下:

1
pyinstaller --onefile yourscript.py

功能就是截图,然后把这一天的所有截图和视频合并为一个视频。

枫叶卷起时间

雨水带着记忆如约而至

晚霞拉长了裙子

等到月亮出来

我才看到你

春从天而降

落在老街

落在小巷

落在柳条上惊起火光

落在少女的肩上发出轰隆隆的巨响

我照亮了烟花

文字淹没了心脏

梦想点燃了柴火

烧的吱吱作响

记忆里人声鼎沸,岁岁平安

回忆如大雨磅礴

流水泛滥

而我

没带伞

鸣叫的月亮

聒噪的星星

遁入土中的蝉

我在看书

书说

最后一页有山川

山川说

我要入海

我要靠岸

你要不动如山

文字在造反

书页上兵荒马乱

我站在油墨里

想靠岸

玫瑰赠予爱人

情书刺入心脏

我亲吻过春天

乘舟而去

哗啦地一声响

黑夜照亮了大地

巨石从海中升起

你说今天已经过了四季

我想扭动世界的齿轮

再一次

再一次

再一次

重新认识你

这个回答下面大多都不是河北考生,在靠着想象写内容。

我是石家庄的考生,回答一下。

首先,要从历史说起。曾经河北的教育基本是三足鼎立的,石家庄、唐山、邯郸,此外保定和沧州也有一些不错的学生,这些地方每年高考成绩分布也比较均匀。但是从90年初期,突然变成了石家庄一枝独秀,甚至是垄断性的优势。

这背后有两个原因,一是90年代初期石家庄扩建了好多学校,比如41、42、43全是那时候建校的,突然多了好多学校,导致老师的匮乏,所以其他城市的一些优秀教师就趁着机会来石家庄了,这其实是石家庄作为省会的一次吸血行为。二是别的城市的好学校一般只有一所,比如唐山一中和邯郸一中,而石家庄一直是三所好学校,石家庄一中、石家庄二中、正定中学,在90年代末期还有了43中和辛集中学的异军突起。石家庄这些学校为了提高整体实力,所以一直在互通有无,搞了很多教学联盟,石家庄学生印象比较深的应该是当时有很多次多校联考。

基于这两点,到了21世纪的头几年里,除了唐山一中和邯郸一中(邯郸一中甚至都没坚持多久)两所学校还能保持不错的成绩外,其他城市的学校几乎全面崩盘了,高分段学生石家庄的占比畸高。甚至很多年里,石家庄的一些二线高中,比如17、24、40、42、43的成绩放在其他城市都是第一第二的水平。

显然其他城市的学生就丧失了机会。在01年到03年之间的某个时间点里,之后河北先后出了两个非常大胆,并且也不好说是不是为了解决这个问题的政策,当然,某种角度解决了石家庄教育资源一家独大的问题 – 变成了石家庄和衡水两家独大。

这两个正常一是默许跨地区招生,二是允许民营资本介入公立教育。后一点我们当时称为公办民助,当时的重点中学都会有两个牌子,一个是公立学校只能在片内招生,一个是民办学校大范围招生,甚至义务教育阶段都在这么干。

但为什么成功的衡水?

要解释这个问题,还要从衡水说起。

外地人可能不知道衡水是什么一个城市,很长时间里,无论经济总量还是人均收入全是河北倒数两名。 我上高中的时候衡水只有一家肯德基(刚开门时是大新闻),没有麦当劳没有必胜客,连一个正经的购物中心都没有,哪怕现在去衡水看着还是像一个大县城。衡水虽然是一个地级市,但是我们当时一直叫衡中是县中,甚至我们把这类死学习的中学统称为县中。

在当时衡中并不是最早搞的,最早做全省掐尖招生的是石家庄二中,让其立刻超过了石家庄一中。石家庄一中从80年代到21世纪初期,都是河北最好的中学,但是石家庄一中一直有一颗搞素质教育的心,并没有参与到这一轮掐尖招生和拼命卷的浪潮中,导致了被全面超越。石家庄一中的结果也证明了在河北的教育环境里你不能搞素质教育。

在当时的这一轮浪潮里,最早成功的县中并不是衡中,是辛集中学,开始时热度比衡中高得多。但最终胜利的是衡中,原因很简单。

在这么一个基础设施极差的地方,如果有一个愿意搞教育的人站出来,当地政府会怎么做?肯定是全面开绿灯,于是就有了衡水中学 + 衡水一中这种看着畸形的发展模式出现,甚至是突破了政策底线的发展模式。

地方政府对这一所学校的保护是难以想象的。而石家庄市区内的学校是享受不到这些政策倾向的,甚至省里施压,衡水也会死扛着,在当时衡中是他们最大的招牌。

当然也不能否认李金池的能力,去精英中学依然可以复制衡水中学的成功模式。但他的能力没有大势重要,哪怕没有他,也会卷出来另外一个类似形态的学校。

所以归根到底无非是三个显而易见的原因,最最最主要的原因是河北的教育资源太匮乏了,从基础教育到高等教育都匮乏,必须要内耗。二是政策上有漏洞,衡中的出现有政策铺垫。三是衡水因为过于贫穷,好不容易出来一所名牌中学,当地给与了最大的保护。

至于有些人认为是民营资本的入侵是根本原因,这显然太 Naive 了,河北的基础教育阶段前两年取消了公办民助的模式,全成了全公立,强制片内入学,结果是好学校变得更强了,差学校变得更烂了,并且穷学生更没机会了。以前那几千块一两万的学费还可能掏得起,现在的学费变成了一套学区房,而那些有钱人的孩子接受的教育资源好,先天成绩就好。高中同样如此,如果限制了民办,那有钱有权的也能继续侵占教育资源,大不了把户口迁过来不就好了,河北省内都是随便落户的。

事实上,衡中早就被严重侵占了,现在在衡水系的中学念书的学生已经很少有穷人的孩子了,基本都是有钱人家的。但并不能说这个政策是错的,除了衡中系,河北还有好多跨区招生的学校,至少能让一些家里条件不好的人来上学,而不是留在本地,和一堆地痞流氓们混日子 – 很多县城的公立学校都要面对这个情况,倒不是说教育质量不好,而是教育环境不好。

现在衡水的武邑中学(武邑中学也是那个时代的卷王学校之一,只不过没卷过衡中)就是这类为相对差一点的学生提供集中教学的中学,一学期学费大几千块,虽然比公立贵很多,但是至少比大部分小城市甚至县城的公立学校教育质量都高,这个学费咬咬牙也能出得起。类似的学校有很多,光武邑就还有另外一个宏达学校(评论有人提醒,其实宏达学校就是武邑中学的民办部分),都是来自县城的孩子,如果留在原地他们根本没有机会接受好的教育,现在把他们集中起来,至少还能有走出河北的可能。

其实,大部分省份都是两三个高中垄断高分档的学生,并且好学校都在省会。你如果是一个小城市家境普通的顶尖学生,在这些省份没准更绝望,因为你12年的教育阶段都没办法和省会的学生拼教育资源,而河北这个政策还给了你一定的可能性。你要你成绩好,就可以接受全省最好的教育。

所以河北的政策本质上是给了一个所有人相对公平一点的卷的权利,然后大家敞开了卷,只是衡中成为了这场养蛊运动的胜利者。

当然,这还是在做自杀式的内耗,这个模式并没有提高整体的升学率,无论怎么做都是在侵占别人的升学率。在这个问题上,怎么想都会发现,仿佛是一个零和游戏,甚至是一个全败的游戏,只是有些人比较惨,有些人特别惨。

所以,真正的错误只有一个,就是社会资源的分配不均。导致教育资源分配不均的也是社会资源不均,去抨击教育部门根本不解决任何根本性问题。

之所以出现在河北,是因为河北过于严重,外部看是没有好大学,内部是社会阶层分化严重,两个叠加起来导致的这个结果。这个问题不解决,教育政策怎么改都不会更好。

本来是准备做算法复健,想起来自己曾经也算是正经用 Lisp 工作过,所以试着用 Lisp 写了个简单的红黑树:

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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217

;; 定义红黑树的节点
(defconstant +red+ 'red)
(defconstant +black+ 'black)

(defstruct rbtree
color
key
left
right
parent)

;; 插入操作的辅助函数:在二叉搜索树中插入新节点
(defun insert-bst (tree node)
(let ((parent nil)
(current tree))
(loop while current do
(setf parent current)
(if (< (rbtree-key node) (rbtree-key current))
(setf current (rbtree-left current))
(setf current (rbtree-right current))))
(setf (rbtree-parent node) parent)
(if (null parent)
(setf tree node) ; 树是空的,新节点成为树根
(if (< (rbtree-key node) (rbtree-key parent))
(setf (rbtree-left parent) node) ; 成为左孩子
(setf (rbtree-right parent) node))) ; 成为右孩子
node))

;; 红黑树的插入操作
(defun insert-rbtree (tree key)
(let ((new-node (make-rbtree :key key :color +red+)))
(if (null tree)
(setf (rbtree-color new-node) +black+)
(insert-bst tree new-node))
(fixup-rbtree new-node)
(root-of new-node))

;; 确保红黑树性质的辅助函数
(defun fixup-rbtree (node)
(loop while (and (rbtree-parent node)
(= +red+ (rbtree-color (rbtree-parent node))))
(let* ((parent (rbtree-parent node))
(grandparent (rbtree-parent parent)))
(if (eq parent (rbtree-left grandparent))
(let ((uncle (rbtree-right grandparent)))
(cond ((and uncle (= +red+ (rbtree-color uncle))) ; 情况1
(setf (rbtree-color parent) +black+)
(setf (rbtree-color uncle) +black+)
(setf (rbtree-color grandparent) +red+)
(setf node grandparent))
((eq node (rbtree-right parent)) ; 情况2
(setf node parent)
(left-rotate node))
(t ; 情况3
(setf (rbtree-color parent) +black+)
(setf (rbtree-color grandparent) +red+)
(right-rotate grandparent))))
(let ((uncle (rbtree-left grandparent))) ; 右子树的情况与左子树相反
(cond ((and uncle (= +red+ (rbtree-color uncle))) ; 情况1
(setf (rbtree-color parent) +black+)
(setf (rbtree-color uncle) +black+)
(setf (rbtree-color grandparent) +red+)
(setf node grandparent))
((eq node (rbtree-left parent)) ; 情况2
(setf node parent)
(right-rotate node))
(t ; 情况3
(setf (rbtree-color parent) +black+)
(setf (rbtree-color grandparent) +red+)
(left-rotate grandparent))))))
(setf (rbtree-color (root-of node)) +black+)) ; 确保根节点是黑色

;; 对节点 x 进行左旋转。
(defun left-rotate (x)
(let ((y (rbtree-right x)))
(setf (rbtree-right x) (rbtree-left y)) ; 将 y 的左子树设置为 x 的右子树
(when (rbtree-left y)
(setf (rbtree-parent (rbtree-left y)) x)) ; 更新 y 左子树的父节点为 x
(setf (rbtree-parent y) (rbtree-parent x)) ; 将 x 的父节点设置为 y 的父节点
(if (null (rbtree-parent x)) ; 如果 x 是根节点
(setf (rbtree-parent y) nil) ; y 成为新的根节点
(if (eq x (rbtree-left (rbtree-parent x))) ; 如果 x 是其父节点的左子节点
(setf (rbtree-left (rbtree-parent x)) y) ; 将 y 设置为 x 的父节点的左子节点
(setf (rbtree-right (rbtree-parent x)) y))) ; 否则,将 y 设置为 x 的父节点的右子节点
(setf (rbtree-left y) x) ; 将 x 设置为 y 的左子节点
(setf (rbtree-parent x) y))) ; 更新 x 的父节点为 y

;; 对节点 y 进行右旋转。
(defun right-rotate (y)
(let ((x (rbtree-left y)))
(setf (rbtree-left y) (rbtree-right x)) ; 将 x 的右子树设置为 y 的左子树
(when (rbtree-right x)
(setf (rbtree-parent (rbtree-right x)) y)) ; 更新 x 右子树的父节点为 y
(setf (rbtree-parent x) (rbtree-parent y)) ; 将 y 的父节点设置为 x 的父节点
(if (null (rbtree-parent y)) ; 如果 y 是根节点
(setf (rbtree-parent x) nil) ; x 成为新的根节点
(if (eq y (rbtree-right (rbtree-parent y))) ; 如果 y 是其父节点的右子节点
(setf (rbtree-right (rbtree-parent y)) x) ; 将 x 设置为 y 的父节点的右子节点
(setf (rbtree-left (rbtree-parent y)) x))) ; 否则,将 x 设置为 y 的父节点的左子节点
(setf (rbtree-right x) y) ; 将 y 设置为 x 的右子节点
(setf (rbtree-parent y) x))) ; 更新 y 的父节点为 x


;; 红黑树的删除操作
(defun transplant (tree u v)
"用节点 v 替换节点 u。"
(if (null (rbtree-parent u))
(setf tree v)
(if (eq u (rbtree-left (rbtree-parent u)))
(setf (rbtree-left (rbtree-parent u)) v)
(setf (rbtree-right (rbtree-parent u)) v)))
(when v
(setf (rbtree-parent v) (rbtree-parent u))))

(defun tree-minimum (node)
"找到以 node 为根的子树的最小节点。"
(loop while (rbtree-left node) do
(setf node (rbtree-left node)))
node)

(defun delete-rbtree (tree key)
"从红黑树中删除键为 key 的节点。"
(let ((z (tree-search tree key)) ; 需要实现 tree-search 函数
(y z)
(y-original-color (rbtree-color y))
x)
(if (null (rbtree-left z))
(progn
(setf x (rbtree-right z))
(transplant tree z (rbtree-right z)))
(if (null (rbtree-right z))
(progn
(setf x (rbtree-left z))
(transplant tree z (rbtree-left z)))
(progn
(setf y (tree-minimum (rbtree-right z)))
(setf y-original-color (rbtree-color y))
(setf x (rbtree-right y))
(if (eq y (rbtree-parent z))
(setf (rbtree-parent x) y)
(progn
(transplant tree y (rbtree-right y))
(setf (rbtree-right y) (rbtree-right z))
(setf (rbtree-parent (rbtree-right y)) y)))
(transplant tree z y)
(setf (rbtree-left y) (rbtree-left z))
(setf (rbtree-parent (rbtree-left y)) y)
(setf (rbtree-color y) (rbtree-color z)))))
(when (eq +black+ y-original-color)
(fixup-delete-rbtree tree x))
tree))

;; 调整树以保持红黑性质。
(defun fixup-delete-rbtree (tree x)
(loop while (and x (not (eq x tree)) (eq +black+ (rbtree-color x)))
(if (eq x (rbtree-left (rbtree-parent x)))
(let ((w (rbtree-right (rbtree-parent x))))
(cond
((eq +red+ (rbtree-color w)) ; 情况1:x的兄弟节点w是红色
(setf (rbtree-color w) +black+)
(setf (rbtree-color (rbtree-parent x)) +red+)
(left-rotate (rbtree-parent x))
(setf w (rbtree-right (rbtree-parent x))))
((and (eq +black+ (rbtree-color (rbtree-left w)))
(eq +black+ (rbtree-color (rbtree-right w)))) ; 情况2:w的两个子节点都是黑色
(setf (rbtree-color w) +red+)
(setf x (rbtree-parent x)))
(t
(when (eq +black+ (rbtree-color (rbtree-right w))) ; 情况3:w的右子节点是黑色
(setf (rbtree-color (rbtree-left w)) +black+)
(setf (rbtree-color w) +red+)
(right-rotate w)
(setf w (rbtree-right (rbtree-parent x))))
(setf (rbtree-color w) (rbtree-color (rbtree-parent x)))
(setf (rbtree-color (rbtree-parent x)) +black+)
(setf (rbtree-color (rbtree-right w)) +black+)
(left-rotate (rbtree-parent x))
(setf x tree))))
(let ((w (rbtree-left (rbtree-parent x)))) ; x 是其父节点的右子节点的情况
(cond
((eq +red+ (rbtree-color w)) ; 情况1:x的兄弟节点w是红色
(setf (rbtree-color w) +black+)
(setf (rbtree-color (rbtree-parent x)) +red+)
(right-rotate tree (rbtree-parent x))
(setf w (rbtree-left (rbtree-parent x))))
((and (eq +black+ (rbtree-color (rbtree-right w)))
(eq +black+ (rbtree-color (rbtree-left w)))) ; 情况2:w的两个子节点都是黑色
(setf (rbtree-color w) +red+)
(setf x (rbtree-parent x)))
(t
(when (eq +black+ (rbtree-color (rbtree-left w))) ; 情况3:w的左子节点是黑色
(setf (rbtree-color (rbtree-right w)) +black+)
(setf (rbtree-color w) +red+)
(left-rotate tree w)
(setf w (rbtree-left (rbtree-parent x))))
(setf (rbtree-color w) (rbtree-color (rbtree-parent x)))
(setf (rbtree-color (rbtree-parent x)) +black+)
(setf (rbtree-color (rbtree-left w)) +black+)
(right-rotate tree (rbtree-parent x))
(setf x tree)))))
(setf (rbtree-color x) +black+))

;; 在以 node 为根的红黑树中查找键为 key 的节点。
(defun tree-search (node key)
(loop while (and node (not (eq key (rbtree-key node))))
do (if (< key (rbtree-key node))
(setf node (rbtree-left node))
(setf node (rbtree-right node))))
node)


;; 找到并返回树的根节点
(defun root-of (node)
(loop while (rbtree-parent node) do
(setf node (rbtree-parent node)))
node)

以上程序写了我整整两天,想当年面试都能直接手写红黑树 …… 其实还有很大的优化空间,同时保不齐还有 Bug。

并且 Common Lisp 的可读性实在太差了。

之前发现,自己做的美术资源经常有一些细微的毛边,虽然不明显,但看起来还是不舒服。起初我以为是抗锯齿的问题,直接开到了最大,完全没有任何变化。试了一下发现,其实解决办法很简单:

  1. 在 Photoshop 出图的时候,图片边缘留1到2个像素的空白区域。
  2. 尽量出图的尺寸是最终需要的尺寸,在 Unity 进行缩放就会出现锯齿感的毛边。理论上换个抗锯齿的算法,或者写个 Shader 也能解决这个问题,但显然最简单、最保险和效率最高的就是控制好出图的尺寸。

在我们做开发时,一般都会专门写一个 GameManager 来管理游戏的数据,但 GameManager 存在也会遇到一个问题。在我们进入下一个场景时,上一个场景的所有 GameObject 都会被销毁,所以就有了单例模式(Singleton)。

下面是最简单的实现:

1
2
3
4
5
6
7
8
9
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }

private void Awake()
{
Instance = this;
}
}

这里要注意,把 GameManager 的 Instance 属性里的 set 设置为 private,是为了保证外部只能读取。

但是这么写以后还是没解决会被销毁的问题,于是需要使用 DontDestroyOnLoad() 方法:

1
2
3
4
5
6
7
8
9
10
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }

private void Awake()
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
}

这么完成以后会发现一个问题,假设我们从场景一到场景二,然后从场景二回到场景一,这时候就会出现两个 GameManager ,所以我们要通过判断,保证场景里永远只存在一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }

private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
}

这样就完成了一个完整的单例模式。

但实际使用中,我们可能大量内容需要使用单例,这时候每个文件都这么写就很麻烦了,所以可以使用一个简单的办法,通过继承一个类来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
public static T Instance { get; private set; }

protected void Awake()
{
if (Instance == null)
{
Instance = (T) this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
}

每次使用的时候就非常方便了:

1
2
3
4
public class GameManager : Singleton<GameManager>
{

}