# 零件开发基础

本节将通过一些内置的零件模板,来介绍零件的生命周期,带领大家初步了解零件开发。

# 例子

# MyLogPart

首先我们创建一个玩家预设,再在新建零件中,找到MyLogPart,进行创建。并将MyLogPart挂接到玩家预设下。

接下来我们可以看到,MyLog这个零件,主要由两个python源代码文件构成,分别是MyLogPart.pyMyLogPartMeta,我们在PyCharm中打开这个项目。

对文件右键,打开文件所在位置,找到这个项目的根目录

将路径复制,在PyCharm中打开,并将资源包的文件夹,设置为Sources Root,否则补全功能将无法正常使用。

接下来按照如图路径打开MyLogPart.py,来观察它的代码。

可以发现,代码中有一个MyLogPart类,继承了PartBase类。在__init__这个初始化函数中,定义了自身的namedescription。这两个均是从PartBase中继承下来的变量,分别代表这个零件的名字,和描述。

所有零件的保留变量名在下方列出了,开发者在对零件进行开发时,要避免使用这些变量名,以防出现不可预料的意外。

  • id

  • classType

  • isClient

  • filterKeys

  • _parent

  • entityId

  • boxId

  • name

  • transform

  • isRemoved

  • loaded

  • needUpdate

  • tickEnable

  • data

  • dataKeys

  • eventMap

  • replicated

而剩下的变量定义,均为这个类的成员变量。

TickClient函数,是一个可重写的函数,它会在客户端被每Tick调用。

并且每次都随机在30到90之间随机一个数字,作为打印输出的间隔。

接下来看MyLogPartMeta.py,它是继承了PartBaseMeta,是用来存储在预设编辑器上的可编辑内容的

例如这里的interval,就对应了MyLogPart.py的interval变量,类型为PVector2,并且可以设置提示文本,具体编写格式会在后面一起介绍。

经过Meta的设置,我们就可以在预设编辑器的零件的属性中,看到对应的打印间隔的设置项。

# ReplicatePart

再次新建一个ReplicatePart零件,继续观察它的代码。

可以看到分裂零件的__init__函数和日志零件的大同小异,同样都是初始化数据。

这里重写了CanAdd函数和InitServer函数,其中InitServer代表服务器初始化,CanAdd会在挂载零件时触发,用来防止零件挂载到错误的预设上。

和日志相比,它多监听了一个实体受伤事件,用来在受伤时进行分裂。

它的PartMeta也是大同小异,定义了两个参数,分别是PBool和PInt类型。

@sunshine_class_meta
class ReplicatePartMeta(PartBaseMeta):
	CLASS_NAME = "ReplicatePart"
	PROPERTIES = {
		"loop": PBool(sort=1000, group="ReplicatePart", text="循环分裂"),
		"health": PInt(sort=1001, group="ReplicatePart", text="血量"),
	}

# 生命周期

零件的生命周期指零件在游戏从开始到结束的整个运行过程。

零件分服务端和客户端,在运行的各个过程,会触发不同的函数。

拿服务端来说,会在初始化阶段先__init__,然后InitServer。初始化完成,便每Tick触发TickServer。在卸载阶段(游戏关闭/区块卸载),UnloadServer。在被击杀或者主动调用Destroy接口之后,触发DestroyServer。客户端同理。

具体每个函数的定义,可以在文档中查看。同时文档中还有别的函数,可以自行查看用法,例如刚刚看到的CanAdd函数

# 自定义属性

自定义属性就是由PartBaseMeta类来定义的。要创建一个自定义属性,我们主要需要有两个步骤。

  1. 在继承了PartBase的类中定义成员变量
  2. 在继承了PartBaseMeta的类中定义PROPERTIES字典,其中的Key为变量名,Value为这个数据对应的变量属性。

当前自定义的零件支持编辑python的所有基本类型,即:整数int,浮点数float,布尔bool,字符串str,字典dict,列表list,除此之外,针对一些特定需求,也提供了相应的支持,如下拉列表选择,多维向量等。

上表列出了目前所有的支持的属性变量,同时列出了对应的Value定义。

每个属性的详细解释可以参考官方文档

@sunshine_class_meta
class ReplicatePartMeta(PartBaseMeta):
	CLASS_NAME = "ReplicatePart"
	PROPERTIES = {
		"loop": PBool(sort=1000, group="ReplicatePart", text="循环分裂"),
		"health": PInt(sort=1001, group="ReplicatePart", text="血量"),
	}

还是拿分裂零件的属性来说,首先要定义一个CLASS_NAME,这些变量是哪个类的成员变量(会自动生成,不需要手动编写)。再定义一个PROPERTIES,定义两个变量loophealth,设置顺序和所属分组以及描述文本。

# 课后作业

  1. 新建空白附加包,创建玩家预设,并创建一个零件,在零件的生命周期的每个阶段(除tick),都打印信息,来观察执行顺序。
  2. 给这个零件设置自定义属性,提供2个参数,分别代表玩家的最大血量和当前血量,并且在玩家发送聊天消息"更新血量"的时候,应用到玩家身上。

# 操作步骤

新建附加包,创建玩家预设的操作在之前已经演示过很多遍,这里直接跳过。

接下来新建一个空零件,命名为PlayerHealthPart,并挂接到玩家预设上。

接下来使用PyCharm打开项目文件夹,设置Sources Root,打开对应的预设python文件。

文件中默认已经重写了一部分生命周期函数,我们可以在这基础上进行修改,每个都添加一个打印的函数,用来输出每个阶段。

并且额外重写UnloadClientUnloadServer,加上对应输出,修改后的代码如下:

# -*- coding: utf-8 -*-
from Preset.Model.GameObject import registerGenericClass
from Preset.Model.PartBase import PartBase


@registerGenericClass("PlayerHealthPartPart")
class PlayerHealthPartPart(PartBase):
    def __init__(self):
        PartBase.__init__(self)
        # 零件名称
        self.name = "空零件"

    def InitClient(self):
        print "InitClient"

    def InitServer(self):
        print "InitServer"

    def TickClient(self):
        pass

    def TickServer(self):
        pass

    def DestroyClient(self):
        print "DestroyClient"

    def DestroyServer(self):
        print "DestroyServer"

    def UnloadClient(self):
        print "UnloadClient"

    def UnloadServer(self):
        print "UnloadServer"

接下来,新建两个变量,分别是health和maxHealth,代表玩家的血量和最大血量,修改这个零件的name。

@registerGenericClass("PlayerHealthPartPart")
class PlayerHealthPartPart(PartBase):
    def __init__(self):
        PartBase.__init__(self)
        self.health = 20
        self.maxHealth = 30
        self.name = "自定义玩家血量零件"

并且在PlayerHealthPartPartMeta.py中修改PROPERTIES,修改后代码如下:

# -*- coding: utf-8 -*-
from Meta.ClassMetaManager import sunshine_class_meta
from Meta.TypeMeta import PInt
from Preset.Model import PartBaseMeta


@sunshine_class_meta
class PlayerHealthPartPartMeta(PartBaseMeta):
    CLASS_NAME = "PlayerHealthPartPart"
    PROPERTIES = {
        "health": PInt(sort=1, text="血量", default=20, group="自定义零件血量"),
        "maxHealth": PInt(sort=2, text="最大血量", default=30, group="自定义零件血量"),
    }

这时打开编辑器,已经可以看到零件的属性面板显示了自定义的属性。

然后再回到PlayerHealthPartPart.py中,编写设置血量的逻辑。新增一个函数,监听ServerChatEvent,那么我们直接判断聊天内容和发送的实体id,是否是我们这个玩家,然后设置给它更新血量。

    def ServerChatEvent(self, args):
        if args["message"] != "更新血量":
            return
        parent = self.GetParent()
        entityId = parent.GetEntityId()
        if args["playerId"] != entityId:
            return
        self.SetEntityAttrValue(entityId, AttrType.HEALTH, self.health)
        self.SetEntityAttrMaxValue(entityId, AttrType.HEALTH, self.maxHealth)

这样我们的血量就设置完成啦。最终代码:

# -*- coding: utf-8 -*-
from Preset.Model.GameObject import registerGenericClass
from Preset.Model.PartBase import PartBase
from mod.common.minecraftEnum import AttrType


@registerGenericClass("PlayerHealthPartPart")
class PlayerHealthPartPart(PartBase):
    def __init__(self):
        PartBase.__init__(self)
        self.health = 20
        self.maxHealth = 30
        self.name = "自定义玩家血量零件"

    def InitClient(self):
        print "InitClient"

    def InitServer(self):
        print "InitServer"

    def TickClient(self):
        pass

    def TickServer(self):
        pass

    def ServerChatEvent(self, args):
        if args["message"] != "更新血量":
            return
        parent = self.GetParent()
        entityId = parent.GetEntityId()
        if args["playerId"] != entityId:
            return
        self.SetEntityAttrValue(entityId, AttrType.HEALTH, self.health)
        self.SetEntityAttrMaxValue(entityId, AttrType.HEALTH, self.maxHealth)

    def DestroyClient(self):
        print "DestroyClient"

    def DestroyServer(self):
        print "DestroyServer"

    def UnloadClient(self):
        print "UnloadClient"

    def UnloadServer(self):
        print "UnloadServer"

我们进入游戏测试,观察日志输出,可以看到有相关的生命周期输出。

并且发送 更新血量,可以看到我们的生命值被更改。

进阶

20分钟

例子

MyLogPart

ReplicatePart

生命周期

自定义属性

课后作业

操作步骤