使用 Python 游玩我的世界。

参考资料来源:各种网络资料和《零基础学 Minecraft 编程》,人民邮电出版社。

37-0.png

开始

环境配置教程:https://www.bilibili.com/video/BV1FG4y1X7SQ

注意在安装 java -jar BuildTools.jar 的时候会打印一大大大坨信息,整个过程大约6分钟,需要梯子。出现如下信息则表示成功:

37-1.png

启动服务器:

1
2
cd D:\mcserver
java -Xms1024M -Xmx1024M -jar spigot-1.19.4.jar

多人游戏,服务器地址127.0.0.1

1
op username   # 给权限

这个时候就可以使用作弊码了。作弊码附在文章末尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# hellofrompy.py
from mcpi.minecraft import Minecraft
import mcpi.block as block
import time

if __name__ == '__main__':
time.sleep(3)
print('连接MC...')
mc = Minecraft.create()

mc.postToChat("hello frome Python")
x, y, z = mc.player.getTilePos()
print(x, y, z)
mc.setBlocks(x, y, z+5, x, y, z+5, 1, 0)

生成方块

Minecraft 坐标信息:

  • 正东:x轴正方向
  • 正南:z轴正方向
  • 正上:y轴正方向
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 16blocks.py
from mcpi.minecraft import Minecraft
import mcpi.block as block
import time

if __name__ == '__main__':
time.sleep(3)

# 连接到 MC
mc = Minecraft.create()

# 得到角色当前位置
x, y, z = mc.player.getTilePos()

# 羊毛
for i in range(16):
mc.setBlock(x+3, y, z+i, 35, i)
# 35是羊毛的代号,0是白色
time.sleep(0.5) # 加个延时以演示

效果如下:

37-2.png

采用更多层循环以生成平面和立体。

多个方块同时放置的函数:

1
mc.setBlocks(x1, y1, z1, x2, y2, z2, blockid, someindex)

(x1, y1, z1)(x2, y2, z2)对应一个长方体的两个对角顶点。根据这两个顶点我们可以绘制出一个由指定方块组成的长方体。

想要一个空心的?缩小一圈用air方块填充。

简单的例子

实时显示玩家的位置:

1
2
3
4
5
6
7
8
9
10
# showpos.py
from mcpi.minecraft import Minecraft
import time

mc = Minecraft.create()

while True:
time.sleep(1)
pos = mc.player.getTilePos()
mc.postToChat("x="+str(pos.x)+" y="+str(pos.y)+" z="+str(pos.z))

注意:若坐标错误,需输入/setworldspawn 0 0 0

玩家进入某一区域,超过3秒弹出:

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
# bump2sky.py
from mcpi.minecraft import Minecraft
import time

x1 = 60 # y = 80
x2 = 63
z1 = -17
z2 = -15
infield = 0

def bump2sky(x, y, z):
for t in range(8):
y += 0.90*t
mc.player.setPos(x+5, y, z)

time.sleep(3)
mc = Minecraft.create()
while True:
time.sleep(1)
pos = mc.player.getTilePos()
if pos.x>=x1 and pos.x<=x2 and pos.z>=z1 and pos.z<=z2:
mc.postToChat("warnning")
infield += 1
else:
infield = 0
if infield > 3:
mc.postToChat("too slow!")
bump2sky(pos.x, pos.y, pos.z)

效果如下:

37-3.gif

绘制任意图像

知道了如何生成方块,就知道了如何绘制任意图像。

37-4.png

对于一张图片,如果我们能根据每一个像素点的 RGB 值找到对应颜色的 Minecraft 方块,就有可能在游戏中绘制该图像。

然而,并不是所有颜色都可以找到对应的 Minecraft 方块。对于找不到的颜色,我们希望有一个替代颜色,这个颜色应当在人眼视觉上相近。

为此,我们需要一个寻找相近颜色的算法。

已经知道,颜色可用 RGB 表示,是否相近的 RGB 值表示的颜色在视觉上相似呢?答案是否定的。但可喜可贺,还是有以 RGB 值计算颜色距离的公式(具体参考此链接(该内容已备份)):

可以根据以上公式编写代码计算两个颜色的距离。不过,我们采用另一种更好的方案。

CIEDE2000算法是CIE(国际照明委员会)于2000年提出的,它是Delta E算法的改进版本。CIEDE2000算法考虑了人眼感知颜色的非线性特性,并将颜色差异分解为亮度、色相和饱和度三个因素,从而更准确地计算颜色之间的相似度。CIEDE2000算法还考虑了颜色对比度的影响,因此在低对比度颜色之间的比较中表现更好。

这个算法已经封装好了,安装 colormath 库:

1
pip install colormath

使用此库需要初步了解颜色空间,并大致知道一些函数接受什么,返回什么,做了什么。

「动态类型一时爽,代码重构火葬场。」

colormath 官方文档:

注意,如果提示:

1
AttributeError: module 'numpy' has no attribute 'asscalar'. Did you mean: 'isscalar'? 

这是因为:

  • numpy的版本过高,需要降版本(高情商)
  • numpy你真就不考虑历史兼容呗(低情商)

降版本的命令:

1
2
pip uninstall numpy
pip install -U numpy==1.22.4

OK,颜色替代的问题解决了。但我们还缺一个映射方案。我们需要将颜色编码映射到具体的 Minecraft 方块的编号上。这方面的资料找起来比较麻烦。我直接给出链接:

以上链接只是名字到编号的映射,我们总不能用眼睛看出物品的颜色编码吧?因此需要配合颜色到名字的映射表(该内容已备份):

注意有些方块是不能用的,它可能是个半砖,可能会随重力掉落,可能会自爆(例如仙人掌,不过仙人掌另有它用,之后再说),甚至可能根本不是一个方块。为了准确性,还需要在游戏里对比,这个比对过程是痛苦的,为了让世界 no more pain,我决定贴出初步整理的映射表:

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
ffffff,80
dcdcdc,35
dcd9d3,155
d5c98c,216
909090,42
faee4d,41
b0a836,19:1
c5c52c,19
2c4199,35:11
8d909e,82
4f4f4f,1:6
848484,35:8
6db015,35:5
d06d8e,35:6
414141,35:7
ffc125,35:4
ba6d2c,35:1
9941ba,35:2
6699d8,35:3
486c98,251:3
416d84,35:9
6d3699,35:10
58412c,35:12
586d2c,35:13
842c2c,35:14
151515,35:15
4b3f26,5:5
7b663e,25
4fbcb7,57
8a8adc,79
667f33,251:13

这个映射表非常粗糙,如果希望有更丰富的色彩表现,请手动修改之。

注意:一些魔改的mcpi版本并不需要此映射表,而可以直接使用物品名称。但是这种mcpi版本与本文不兼容,也不与《零基础学 Minecraft 编程》兼容。更重要的,它们主要是通过某盘传播(而不是pip安装),如果你和我一样抵制某盘,不想在上面花钱,尽量不要倒向这个版本。

下面放出完整代码:

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
from colormath.color_objects import sRGBColor, LabColor
from colormath.color_diff import delta_e_cie2000
from colormath import color_conversions
from imageio import imread
from mcpi.minecraft import Minecraft

def rgb2hex(red, green, blue):
"""
返回一个 HexColorString
"""
return '{:02x}{:02x}{:02x}'.format(red, green, blue)

def hex2lab(hx):
color_srbg = sRGBColor.new_from_rgb_hex(hx)
color_lab = color_conversions.convert_color(color_srbg, LabColor)
return color_lab

def closest_color(hexStr):
"""
接受一个 HexColorString
返回列表中与该颜色最接近的颜色
"""
if hexStr in colorhave: return colorhave[hexStr]

closest_color = None
closest_distance = None
labStr = hex2lab(hexStr)

for color_code, color_name in colorhave.items():
color_c = hex2lab(color_code)
distance = delta_e_cie2000(labStr, color_c)
if closest_distance is None or distance < closest_distance:
closest_distance = distance
closest_color = color_name

return closest_color

def wip(r, g, b):
s = closest_color(rgb2hex(r, g, b))
tmpls = []
if ':' in s:
tmpls.append(eval(s.split(":")[0]))
tmpls.append(eval(s.split(":")[1]))
else:
tmpls.append(eval(s))
return tmpls

fo = open("D:\mypycode\mccode\colormap.txt")
ls = []
for line in fo:
line = line.replace("\n","")
ls.append(line.split(","))
fo.close()

colorhave = {}
for i in range(len(ls)):
colorhave[ls[i][0]] = ls[i][1]

im = imread('D:\mypycode\imageiostuff\\rmdpic.jpg')
h, w, _ = im.shape

mc = Minecraft.create()
x0, y0, z0 = mc.player.getTilePos()
x0 += 5

for y in range(h):
for x in range(w):
r, g, b = im[y][x]
flagls = wip(r, g, b)
if len(flagls)==1:
mc.setBlock(x0+x, y0+h-y, z0, flagls[0])
else:
mc.setBlock(x0+x, y0+h-y, z0, flagls[0], flagls[1])

一个简单的优化是:把最相近颜色存储起来。

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
from colormath.color_objects import sRGBColor, LabColor
from colormath.color_diff import delta_e_cie2000
from colormath import color_conversions
from imageio.v2 import imread
from mcpi.minecraft import Minecraft

def rgb2hex(red, green, blue):
"""
返回一个 HexColorString
"""
return '{:02x}{:02x}{:02x}'.format(red, green, blue)

def hex2lab(hx):
color_srbg = sRGBColor.new_from_rgb_hex(hx)
color_lab = color_conversions.convert_color(color_srbg, LabColor)
return color_lab

closestcolor = {}
def closest_color(hexStr):
"""
接受一个 HexColorString
返回列表中与该颜色最接近的颜色
"""
if hexStr in colorhave: return colorhave[hexStr]
if hexStr in closestcolor: return closestcolor[hexStr]

closest_color = None
closest_distance = None
labStr = hex2lab(hexStr)

for color_code, color_name in colorhave.items():
color_c = hex2lab(color_code)
distance = delta_e_cie2000(labStr, color_c)
if closest_distance is None or distance < closest_distance:
closest_distance = distance
closest_color = color_name

closestcolor[hexStr] = closest_color
return closestcolor[hexStr]

def wip(r, g, b):
s = closest_color(rgb2hex(r, g, b))
tmpls = []
if ':' in s:
tmpls.append(eval(s.split(":")[0]))
tmpls.append(eval(s.split(":")[1]))
else:
tmpls.append(eval(s))
return tmpls

fo = open("D:\mypycode\mccode\colormap.txt")
ls = []
for line in fo:
line = line.replace("\n","")
ls.append(line.split(","))
fo.close()

colorhave = {}
for i in range(len(ls)):
colorhave[ls[i][0]] = ls[i][1]

im = imread('D:\mypycode\imageiostuff\delisha2.jpg')
h, w, _ = im.shape

mc = Minecraft.create()
x0, y0, z0 = mc.player.getTilePos()
x0 += 5

for y in range(h):
for x in range(w):
r, g, b = im[y][x]
flagls = wip(r, g, b)
if len(flagls)==1:
mc.setBlock(x0+x, y0+h-y, z0, flagls[0])
else:
mc.setBlock(x0+x, y0+h-y, z0, flagls[0], flagls[1])

当然,更好的方案是在命令中传入要绘制的图片,而不是每次绘制都要手动修改代码。

看一下效果(图片太大了两边加载不出来):

37-5.png

作为对比,给出原图:

37-5dot5.jpg

头发的细节:

37-6.png

考虑如下生成的图片:

37-7.png

我们希望只保留这个谜之柴犬,那么可以在数据文件中添加仙人掌的记录。因仙人掌自爆的特性,只会留下柴犬本犬:

37-8.png

对于更大的图片,可能需要在平坦的世界绘制,或者铺在地上。对于像素更高的图片,可能需要事先压缩,或者转换为像素画形式。普通图片转换为像素画形式可以使用 Python 实现,不再赘述。

37-9.png

从上图可以看出,这个颜色映射表还是非常简陋的。但我没耐心再一个个地比对了。

作弊码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/gamerule doDaylightCycle false  # 永久白天
/gamemode creative # 创造
/gamemode survival # 生存
/gamemode adventure # 冒险
/gamemode spectator # 旁观
/spawnpoint # 重生点设置为当前位置
/seed # 种子
/give username minecraft:diamond_ore 64 # 钻石
/give username minecraft:emerald_block 64 # 绿宝石
/tp username {x} {y} {z} # 传送
/time set 0 # morning 6
/time set 9500 # noon
/time set 12000 # 黄昏
/xp {int} # 给经验
/effect username {type} {time} {level} # 给药
/gamerule keepInventory true # 死亡不掉落
/summon minecraft:zombie # 召唤僵尸
/setblock {x} {y} {z} minecraft:redstone # 设置红石方块