本经验内容参考LibGui的GitHub:
列表组件
初始化
io.github.cottonmc.cotton.gui.widget.WListPanel是LibGui提供的列表组件。列表组件需要在建立时提供其中的内容,包括需要显示的列表化数据和显示每一个数据的子组件。
比如,你可以这样声明列表组件,并且暂不初始化。
var gamerulesBooleanListWidget: WListPanel<GameRules.Key<GameRules.BooleanRule>, GameruleBooleanItemPanel>? = null
var gamerulesIntListWidget: WListPanel<GameRules.Key<GameRules.IntRule>, GameruleIntItemPanel>? = null
自然而然的,GameruleBooleanItemPanel和GameruleIntItemPanel就是分别在两个列表中显示数据的子组件了。众所周知,Minecraft中的gamerules有boolean和int两种类型,因此我们需要两个不同的列表和两种不同的子组件。
配置器
然后,我们还需要一个方法用来教会列表组件如何初始化其中的子组件。根据官方的简约(并且不全)的wiki,你可以这样写一个叫做configurator(配置器)的方法,作用也非常形象:
BiConsumer<String, PortalDestination> configurator = (String s, PortalDestination destination) -> {
destination.label.setText(Text.literal(s));
destination.cost.setText(Text.literal("1000 xp"));
destination.sprite.setImage(new Identifier("libgui-test:portal1.png"));
};
此configurator接收两个参数,先后为String和PortalDestination,是由于wiki的前面假定了列表中的每个元素都是字符串,同时子组件类型为PortalDestination。逻辑:LibGui帮我们实例化子组件,然后将数据中的元素与子组件一起传给configurator,由我们告诉他怎么将元素填写到子组件中。
然后,wiki教我们这样创建列表,使用刚才的configurator和一组字符串数据:
ArrayList<String> data = new ArrayList<>();
data.add("Wolfram Alpha");
data.add("Strange Home");
WListPanel<String, PortalDestination> list = new WListPanel<>(data, PortalDestination::new, configurator);
list.setListItemHeight(2*18);
root.add(list, 0, 2, 7, 6);

子组件PortalDestination是在别处设计然后引用的,这里就不再额外复制了。
用Kotlin写gamerules列表
数据
用Kotlin的话同样是数据、配置器、实例化三步,其中数据是gamerules,需要从Minecraft源码中扒,这里就直接展示AI扒出来的成果了:
val gamerulesBoolean = listOf(
GameRules.DO_FIRE_TICK,
GameRules.ALLOW_FIRE_TICKS_AWAY_FROM_PLAYER,
GameRules.DO_MOB_GRIEFING,
GameRules.KEEP_INVENTORY,
GameRules.DO_MOB_SPAWNING,
GameRules.DO_MOB_LOOT,
GameRules.PROJECTILES_CAN_BREAK_BLOCKS,
GameRules.DO_TILE_DROPS,
GameRules.DO_ENTITY_DROPS,
GameRules.COMMAND_BLOCK_OUTPUT,
GameRules.NATURAL_REGENERATION,
GameRules.DO_DAYLIGHT_CYCLE,
GameRules.LOG_ADMIN_COMMANDS,
GameRules.SHOW_DEATH_MESSAGES,
GameRules.SEND_COMMAND_FEEDBACK,
GameRules.REDUCED_DEBUG_INFO,
GameRules.SPECTATORS_GENERATE_CHUNKS,
GameRules.DISABLE_PLAYER_MOVEMENT_CHECK,
GameRules.DISABLE_ELYTRA_MOVEMENT_CHECK,
GameRules.DO_WEATHER_CYCLE,
GameRules.DO_LIMITED_CRAFTING,
GameRules.ANNOUNCE_ADVANCEMENTS,
GameRules.DISABLE_RAIDS,
GameRules.DO_INSOMNIA,
GameRules.DO_IMMEDIATE_RESPAWN,
GameRules.DROWNING_DAMAGE,
GameRules.FALL_DAMAGE,
GameRules.FIRE_DAMAGE,
GameRules.FREEZE_DAMAGE,
GameRules.DO_PATROL_SPAWNING,
GameRules.DO_TRADER_SPAWNING,
GameRules.DO_WARDEN_SPAWNING,
GameRules.FORGIVE_DEAD_PLAYERS,
GameRules.UNIVERSAL_ANGER,
GameRules.BLOCK_EXPLOSION_DROP_DECAY,
GameRules.MOB_EXPLOSION_DROP_DECAY,
GameRules.TNT_EXPLOSION_DROP_DECAY,
GameRules.WATER_SOURCE_CONVERSION,
GameRules.LAVA_SOURCE_CONVERSION,
GameRules.GLOBAL_SOUND_EVENTS,
GameRules.DO_VINES_SPREAD,
GameRules.ENDER_PEARLS_VANISH_ON_DEATH,
GameRules.TNT_EXPLODES,
GameRules.LOCATOR_BAR,
GameRules.PVP,
GameRules.ALLOW_ENTERING_NETHER_USING_PORTALS,
GameRules.SPAWN_MONSTERS,
GameRules.COMMAND_BLOCKS_ENABLED,
GameRules.SPAWNER_BLOCKS_ENABLED
)
val gamerulesInt = listOf(
GameRules.RANDOM_TICK_SPEED,
GameRules.SPAWN_RADIUS,
GameRules.MAX_ENTITY_CRAMMING,
GameRules.MAX_COMMAND_CHAIN_LENGTH,
GameRules.MAX_COMMAND_FORK_COUNT,
GameRules.COMMAND_MODIFICATION_BLOCK_LIMIT,
GameRules.PLAYERS_NETHER_PORTAL_DEFAULT_DELAY,
GameRules.PLAYERS_NETHER_PORTAL_CREATIVE_DELAY,
GameRules.PLAYERS_SLEEPING_PERCENTAGE,
GameRules.SNOW_ACCUMULATION_HEIGHT,
GameRules.MINECART_MAX_SPEED // Minecraft Max Speed可能获取失败,已经在 GameruleIntItemPanel 中针对获取失败进行额外处理。
)
Mojang并没有在游戏源码中将GameRules设计成枚举,(似乎)也没有提供获取全部gamerules的方法,并且int和boolean类型的规则也不方便混用,所以芒果还是采用了比较笨的方法,并在我(和AI)的帮助下取得了阶段性成功。
以上就是在Minecraft 1.21.10中的全部gamerules了(乐)
子组件
以布尔值的规则为例,其子组件可以设计为一个Label与一个Button,在Label中显示规则名称,点击Button切换True或False。由于LibGui提供的组件在添加ToolTip时有些不方便,因此下面的代码中使用FLabel和FButton代替了WLabel和WButton。
@Environment(EnvType.CLIENT)
class GameruleBooleanItemPanel : WPlainPanel () {
val logger: Logger = LoggerFactory.getLogger("GameruleBooleanItemPanel")
var ruleNameLabel: FLabel? = null
var ruleToggleButton: FButton? = null
var ruleValue: Boolean? = null
set(value) {
// 执行gamerule命令
// 花了好久好久才终于从源码里找到这个执行命令的方法!!理论上只要有权限,在单人游戏中和服务器里都可以执行~
if (field != null) {
// 初始化此Panel时当然不应该执行命令啦
MinecraftClient.getInstance().networkHandler!!.sendChatCommand(
"gamerule ${rule!!.name} ${if (value == true) "true" else "false"}"
)
}
// TODO:检查命令是否执行成功,如果没有成功则不应该更改状态
field = value
ruleToggleButton!!.label = Text.literal(if (value == true) "TRUE" else "FALSE")
.withColor(if (value == true) 0x00FF00 else 0xFF0000)
}
// 设置 gamerule
var rule: GameRules.Key<GameRules.BooleanRule>? = null
set(value) {
field = value
ruleNameLabel!!.text = Text.literal(value!!.name)
ruleNameLabel!!.addTooltip(Text.translatable(value.translationKey))
ruleValue = MinecraftClient.getInstance().server!!.gameRules.getBoolean(value)
}
init {
setSize(256, 18)
ruleNameLabel = FLabel(Text.literal("TvT"))
ruleNameLabel!!.setVerticalAlignment(VerticalAlignment.CENTER)
ruleToggleButton = FButton(Text.literal("OvO"))
ruleToggleButton!!.setOnClick { toggle() }
add(ruleNameLabel, 0, 0, 180, 18)
add(ruleToggleButton, 180, 0, 50, 18)
}
fun toggle() {
if (ruleValue != null){
ruleValue = !ruleValue!!
logger.info("Change gamerule ${rule!!.name} to $ruleValue")
}
else {
logger.warn("Gamerule value is null?")
}
}
}
使用rule存储规则,ruleNameLabel和ruleToggleButton分别是Label和Button,ruleValue为规则的值,其中rule和ruleValue分别实现了setter方法。最后,让Button点击触发toggle(),大功告成。
设计setter方法的好处就是……
配置器
val gamerulesBooleanConfigurator = { gamerule: GameRules.Key<GameRules.BooleanRule>, itemPanel: GameruleBooleanItemPanel ->
itemPanel.rule = gamerule
}
val gamerulesIntConfigurator = { gamerule: GameRules.Key<GameRules.IntRule>, itemPanel: GameruleIntItemPanel ->
itemPanel.rule = gamerule
}
实例化列表并添加到布局
在GamerulesDescription的init中:
init {
setRootPanel(root)
root.setSize(256, 180)
gamerulesBooleanListWidget = WListPanel(gamerulesBoolean, ::GameruleBooleanItemPanel, gamerulesBooleanConfigurator)
gamerulesBooleanListWidget!!.setListItemHeight(18)
gamerulesBooleanListWidget!!.parent = root
gamerulesBooleanListWidget!!.setSize(240, 180)
gamerulesIntListWidget = WListPanel(gamerulesInt, ::GameruleIntItemPanel, gamerulesIntConfigurator)
gamerulesIntListWidget!!.setListItemHeight(18)
gamerulesIntListWidget!!.parent = root
gamerulesIntListWidget!!.setSize(240, 180)
root.add(gamerulesBooleanListWidget) { builder ->
builder.title(Text.translatable("gamehelper.screen.gamerules_tab.boolean"))
}
root.add(gamerulesIntListWidget) { builder ->
builder.title(Text.translatable("gamehelper.screen.gamerules_tab.int"))
}
root.validate(this)
}
效果


更多
FLabel和FButton到底干了什么?
package cn.mangofanfan.gamehelper.client.screen.libgui
import io.github.cottonmc.cotton.gui.widget.TooltipBuilder
import io.github.cottonmc.cotton.gui.widget.WLabel
import net.fabricmc.api.EnvType
import net.fabricmc.api.Environment
import net.minecraft.text.MutableText
import net.minecraft.text.Text
@Environment(EnvType.CLIENT)
class FLabel(translate: MutableText): WLabel(translate) {
var tooltipText: Text? = null
fun addTooltip(text: Text) {
tooltipText = text
}
override fun addTooltip(tooltip: TooltipBuilder?) {
if (tooltipText == null) return
tooltip?.add(tooltipText)
}
}
这是FLabel。其实只是做了一些小改动,方便向其中添加工具提示。这是芒果在多次尝试添加ToolTip失败之后大调查GitHub issues然后在Tooltips Don’t Work · Issue #210 · CottonMC/LibGui里面找到的原理,然后用Kotlin封装的结果。
列表的原理
wiki表示LibGui的列表组件需要传入每个子组件的高度,并且滚动的过程没有动画。因此,芒果合理推测此列表组件的原理是首先计算同屏能显示的子组件数量,然后在滚动时用新的组件代替旧的组件,实现子组件的更迭……
当然我还没读LibGui的源码,主要是找gamerules已经让我筋疲力竭了……阿巴巴








暂无评论内容