- 积分
- 20213
- 帖子
- 主题
- 精华
贡献- 份
爱心- 心
- 钻石
- 颗
- 人气
- 点
- 下界之星
- 枚
- 最后登录
- 1970-1-1
- 注册时间
- 2015-8-23
来自:安徽 | 本帖最后由 ustc_zzzz 于 2017-1-19 13:45 编辑
引言:
LiteMod,顾名思义,专指依赖于一个名为LiteLoader的Mod框架的Mod。
和知名框架Forge相比,LiteLoader有着以下优势:
- 轻量级。LiteLoader本身大小不到1MB,只作用于Minecraft客户端,对Minecraft的修改相对少些
- 更新及时。LiteLoader的更新速度一直相较Forge更快一筹,例如LiteLoader的1.11.2版本的第一次构建时间就比Forge早三天
如果你希望打造一个服务端插件和客户端Mod配合的系统,那么在客户端Mod方面,LiteLoader应该是一个不错的选择。
可以去LiteLoader的官方网站了解到关于LiteLoader的一切。
当然,这篇教程充其量只相当于指路,教程不会也没有办法包办有关于LiteMod的一切,更多的内容还需要开发者自己摸索。 如果读者之前有过开发Forge Mod的经验,阅读该教程会相对容易些。
配置开发环境:
比较官方的方法是:
- 从LiteLoader的官网上拷贝一份LiteLoader作者提供的示例Mod:
- git clone http://develop.liteloader.com/liteloader/ExampleMod.git
复制代码
- 去下载一个Gradle
- 进入到你刚刚拷贝的目录下。由于你十有八九用不到LiteLoader官方提供的Git提交历史,所以你大可以把Git文件夹删掉:
- 进行构建参数选项的调整,具体见下一段
- 在你刚刚拷贝的目录下运行:
- gradle setupDecompWorkspace
复制代码
- 等。
如果你之前有过配置Forge Mod的开发环境的经验的话,你应该知道上面这一个字的千钧之力。 等到出现“BUILD SUCCESSFUL”字样的话,就可以进行下一步了,否则。。。再试一次?或者挂个代理什么的 - 如果你使用Eclipse,那么运行:
如果你使用IntelliJIDEA,那么运行:
然后接着等,直到同样的“BUILD SUCCESSFUL”字样出现
如果你懒得下Gradle,懒得从LiteLoader的官方仓库Clone的话。。。你可以:
- 教程后面有关于示例源代码的ZIP,把它下载下来然后省略前三步
- 进行构建参数选项的调整,具体见下一段
- 接着的配置方式一样,只不过要把“gradle”换成“./gradlew”(Linux和OSX)或者“gradlew.bat”(Microsoft Windows)
- 但是等这一步你是躲不过去的,Wish you good luck
|
构建参数选项:
打开“build.gradle”文件,先找到“minecraft”开头的一段:
- minecraft {
- version = "1.11.2"
- mappings = "snapshot_20161220"
- runDir = "run"
- }
复制代码 把version换成你想要的Minecraft版本(教程用的是发帖时最新的1.11.2,你可以按照你想要用的自己调整)。 mappings是MCP版本,由于Minecraft的源代码是被混淆过的,所以MCP就是用于把不好看的混淆名和好看的未混淆名一一对应上的映射表。 这个mapping每天晚上都会更新,所以你其实大可以把这个改成你昨天的日期对应的snapshot(由于时区原因,你当天是拿不到对应的snapshot构建的)。 对于旧版本而言,你可以按照这里给出来的版本和映射表之间的关系选择你想要的版本。 这里的版本大多以stable开头,相当于说已经稳定了。 runDir是最后运行时的目录,保持默认的就行。 然后我们看“litemod”开头的一段:
- litemod {
- json {
- name = "litemodtutor"
- version = project.version
- mcversion = "1.11.2"
- author = "ustc_zzzz"
- description = "LiteModTutor"
- }
- }
复制代码 我们先说一下这段是做什么的。
读者如果有对开发其他类型的Mod或插件熟悉的话,应该知道每个最终发布的JAR都会有一个对应的文件表示该Mod或插件。 Bukkit插件中这个是“plugin.yml”,而Forge Mod和Sponge插件中这个是“mcmod.info”,对于LiteMod来说,这个文件就是“litemod.json”。这段的作用就是生成这个JSON,你也可以在“src/main/resources”文件夹下找到这个JSON,不过由于它可以完全由上面那段生成,所以你大可以删掉那个JSON文件(最后提供的示例源代码ZIP中这个文件已经被我删掉了)。
这段的内容还是很好理解的,如果你Clone得到的对应文件的这一段有一行是“mixin”开头的话,那你暂时用不着它,删掉就是。 然后我们来看下面这一段:
- version = "0.1.0"
- group = "com.github.ustc_zzzz.litemodtutor" // http://maven.apache.org/guides/mini/guide-naming-conventions.html
- archivesBaseName = "litemodtutor"
复制代码 首先,“version”。它代表你的Mod版本,待会在你的Mod里你也要指定这个版本号,推荐的版本号格式是语义化版本。 然后是“group”。这个“group”应该和你待会写代码时用到的包名一致,然后关于包名的约定,这行的注释里的链接已经讲得比较清楚了。 最后是“archivesBaseName”。这和你最后生成的Mod文件名有关。 然后你可以删掉一些没有用的内容,最后我把我写的“build.gradle”文件整体列在这里,读者可以把它直接替换掉现有的“build.gradle”文件并进行修改:
- buildscript {
- repositories {
- mavenCentral()
- maven {
- name = "sonatype"
- url = "https://oss.sonatype.org/content/repositories/snapshots/"
- }
- maven {
- name = "forge"
- url = "http://files.minecraftforge.net/maven"
- }
- }
- dependencies {
- classpath 'net.minecraftforge.gradle:ForgeGradle:2.2-SNAPSHOT'
- }
- }
- apply plugin: 'net.minecraftforge.gradle.liteloader'
- version = "0.1.0"
- group = "com.github.ustc_zzzz.litemodtutor" // http://maven.apache.org/guides/mini/guide-naming-conventions.html
- archivesBaseName = "litemodtutor"
- minecraft {
- version = "1.11.2"
- mappings = "snapshot_20161220"
- runDir = "run"
- }
- litemod {
- json {
- name = "litemodtutor"
- version = project.version
- mcversion = "1.11.2"
- author = "ustc_zzzz"
- description = "LiteModTutor"
- }
- }
- jar {
- from litemod.outputs
- }
复制代码 |
构建运行:
构建还是很简单的。运行下面的命令(同样在部分情况下把“gradle”换成“./gradlew”或“gradlew.bat”):
然后等到出现“BUILD SUCCESSFUL”字样,你就可以在“build/libs”里找到一个“.litemod”后缀的文件,拿去用就行了。
运行的话除了在IDE里运行,你还可以运行命令:
第一次运行时会弹出来一个小窗,你只要把你的正版用户名和密码输进去就行了:
 如果不愿意使用正版账号,你可以在这个小窗左下角打勾,然后就可以以离线方式登录了。
如果上面的命令你已经运行过一次了,下一次运行时可以直接加上“--offline”参数,以减少不必要的等待。 |
主类格式与事件的监听:
LiteLoader识别Mod主类格式的方式是找到一个类实现了“com.mumfrey.liteloader.LiteMod”,并以“LiteMod”开头,作为Mod的主类。 不过。。。LiteLoader官方给的示例Mod是这么写的:
- public class LiteModExample implements Tickable, PreRenderListener
复制代码 说好的“LiteMod”接口呢? 实际上是这样子的:
- public interface Tickable extends LiteMod
- public interface PreRenderListener extends LiteMod
复制代码 好我们继续。再重复一遍,和Forge等Mod框架不同,LiteLoader识别Mod主类的方式是该类实现了“LiteMod”接口,并以“LiteMod”开头。 那么这些继承了“LiteMod”的接口又是做什么的呢?
熟悉开发插件以及Forge Mod的开发者应该比较熟悉注册事件的方式,在其中,用于执行事件逻辑的类中的方法,都加上了诸如“@EventHandler”或者“@SuscribeEvent”的注解,不过LiteLoader不是这么做的,LiteLoader的事件是基于接口的。换句话说,每种类型的事件都有其特定的接口,Mod主类只要实现了相应的接口,就代表监听了相应的事件。
如果有读者对旧版本的LiteLoader比较熟悉,就应该知道LiteLoader中历史最悠久,也是最知名的接口之一,就是一个名为“Tickable”的接口。该接口在每次客户端执行一次渲染时调用一次,也就是说该接口每个tick会调用多次。我们看看这个接口新添加的方法:
- public abstract void onTick(Minecraft minecraft, float partialTicks, boolean inGame, boolean clock);
复制代码
第一个参数是Minecraft的主类不解释。 第二个参数代表当前进行的tick进行了多久。因为刚刚我们说到该方法会在一个tick执行多次,所以说这个浮点数就代表执行时tick的小数部分,永远在0和1之间。 第三个参数表示我们是否在游戏中,也就是是否在一些诸如按下Esc出现的界面的状态。 最后一个参数如果为真,代表它是一个新的tick,否则代表它是当前tick的又一次执行。
通常的渲染HUD的方式为:
- @Override
- public void onTick(Minecraft minecraft, float partialTicks, boolean inGame, boolean clock)
- {
- if (inGame && minecraft.currentScreen == null && Minecraft.isGuiEnabled())
- {
- // DO SOMETHING
- }
- }
复制代码 这保证了HUD能够像游戏中的其他HUD一样渲染。
这里作为示例,我们想要制作的Mod是一个像弹幕(Danmaku)视频网站一样的Mod。服务端插件发送弹幕,同时在客户端显示出来。 我们先完成一下弹幕的逻辑(这里偷懒直接使用内部类了)。这段代码只是用于实现游戏逻辑的,具体内容是什么样子的不重要:
- private DanmakuRenderer danmakuRenderer;
- @Override
- public void onTick(Minecraft minecraft, float partialTicks, boolean inGame, boolean clock)
- {
- if (inGame && minecraft.currentScreen == null && Minecraft.isGuiEnabled())
- {
- ScaledResolution scaledresolution = new ScaledResolution(minecraft);
- int width = scaledresolution.getScaledWidth(), height = scaledresolution.getScaledHeight();
- this.danmakuRenderer.render(minecraft, width, height, partialTicks);
- }
- }
- private static class DanmakuRenderer
- {
- private final float movePerTick;
- private final Queue<Danmaku> strings = new ConcurrentLinkedQueue<Danmaku>();
- private DanmakuRenderer(float movePerTick)
- {
- this.movePerTick = movePerTick;
- }
- private void push(Minecraft minecraft, String string)
- {
- if (minecraft.world != null)
- {
- this.strings.add(new Danmaku(string, minecraft.world.getTotalWorldTime()));
- }
- }
- private void render(Minecraft minecraft, int width, int height, float partialTicks)
- {
- long now = minecraft.world.getTotalWorldTime();
- Iterator<Danmaku> iterator = this.strings.iterator();
- while (iterator.hasNext())
- {
- Danmaku danmaku = iterator.next();
- String text = danmaku.string;
- float timeDiff = now - danmaku.renderTick + partialTicks;
- if (timeDiff < 0)
- {
- break;
- }
- int offsetWidth = (int) (width - timeDiff * this.movePerTick);
- int offsetHeight = height / 2;
- FontRenderer fontRenderer = minecraft.fontRendererObj;
- int stringWidth = fontRenderer.getStringWidth(text);
- if (offsetWidth < -stringWidth)
- {
- iterator.remove();
- }
- else
- {
- fontRenderer.drawString(text, offsetWidth, offsetHeight, 0xFFFFFFFF);
- }
- }
- }
- }
- private static class Danmaku
- {
- private final String string;
- private final long renderTick;
- private Danmaku(String string, long renderTick)
- {
- this.string = string;
- this.renderTick = renderTick;
- }
- }
复制代码 我们暂时把“DanmakuRenderer”类的“push”方法闲置,以供接下来使用。
很好,下面我们来考查“LiteMod”接口(及其父接口)定义的四个方法:
- @Override
- public abstract String getName();
- public abstract String getVersion();
- public abstract void init(File configPath);
- public abstract void upgradeSettings(String version, File configPath, File oldConfigPath);
复制代码
getName方法用于显示名称(没错就是游戏主界面右侧的小鸡界面里的名称)。 getVersion方法用于设置版本号(这里最好和在“build.gradle”里设置的相同)。 init方法会在游戏尚未完全初始化时调用,用于对Mod进行初始化(这里不要轻易操作原版Minecraft的类)。传入的参数一般情况下代表“liteconfig/common”文件夹,你可以在这里读取并写入配置,不过我会在后面的内容中提供一种更好的方法。 upgradeSettings方法会在Minecraft发生版本更新时调用。如果LiteLoader没有找到当前Minecraft版本的配置文件却找到了旧的Minecraft版本的配置文件,就会调用这个方法。传入的第一个参数代表新的Minecraft版本,后两个参数一般情况下代表“liteconfig/config.{Minecraft版本}”文件夹。比如说这个1.11.2的Mod如果运行的整合包从1.10.2升级而来,那么对应的三个参数分别为“1.11.2”、代表“liteconfig/config.1.11.2”的文件夹,代表“liteconfig/config.1.10.2”的文件夹。
读者可能会有疑问:为什么同时有“liteconfig/common”和“liteconfig/config.{Minecraft版本}”两种文件夹呢?它们的区别是什么呢?别急,后面会讲到的。
然后我们实现一下这四个方法。这里我们利用“init”方法完成了“danmakuRenderer”字段的初始化:
- private float movePerTick = 5.0F;
- @Override
- public String getName()
- {
- return "LiteModTutor";
- }
- @Override
- public String getVersion()
- {
- return "0.1.0";
- }
- @Override
- public void init(File configPath)
- {
- this.danmakuRenderer = new DanmakuRenderer(this.movePerTick);
- }
- @Override
- public void upgradeSettings(String version, File configPath, File oldConfigPath)
- {
- // DO NOTHING
- }
复制代码
最后说一句,Mod主类的构造方法必须是无参的,请在“init”方法完成所有的初始化操作。
|
与服务端插件交互:
在Minecraft中,各种各样的数据包从网络中得到,进行处理后,再变成新的数据包发送进网络中去。其中有一种名为“CustomPayload”的数据包尤为特殊,因为它是Minecraft专门为插件和Mod的交互提供的。该数据包包含一个用作识别符的名称字符串,以及一串字节流。这种数据包还有一个名字,就是PluginChannel。
一些常见的插件和Mod交互的例子,如WorldEdit插件和WorldEditCUI客户端Mod之间,就是用这种方式进行的交互。LiteLoader里用于接收服务端插件发送的数据包的接口为“PluginChannelListener”。这个接口(及其父接口)相较“LiteMod”接口多提供了两个方法:
- @Override
- public abstract List<String> getChannels();
- public abstract void onCustomPayload(String channel, PacketBuffer data);
复制代码 第一个方法用于指定监听的所有PluginChannel的名称集合。 第二个方法用于指定收到特定PluginChannel的数据时的操作。
我们先在第一个方法指定我们想要监听的PluginChannel的名字,再在第二个方法把弹幕的数据取出来并等待显示:
- private static final String CHANNEL_LITEMODTUTOR = "LITEMODTUTOR";
- @Override
- public List<String> getChannels()
- {
- return Collections.singletonList(CHANNEL_LITEMODTUTOR);
- }
- @Override
- public void onCustomPayload(String channel, PacketBuffer data)
- {
- if (CHANNEL_LITEMODTUTOR.equals(channel))
- {
- byte[] bytes = data.readByteArray();
- String text = new String(bytes, Charsets.UTF_8);
- this.danmakuRenderer.push(Minecraft.getMinecraft(), text);
- }
- }
复制代码
作为对应,我们编写相应的服务端插件。由于作者对Bukkit插件不熟悉,所以使用的是Sponge插件:
- /**
- * [url=home.php?mod=space&uid=1231151]@author[/url] ustc_zzzz
- */
- @Plugin(id = LiteModTutorPlugin.ID, name = "LiteModTutorPlugin", authors =
- {"ustc_zzzz"}, version = "0.1.0", description = "LiteModTutorPlugin")
- public class LiteModTutorPlugin
- {
- public static final String ID = "litemodtutorplugin";
- private static final String CHANNEL_LITEMODTUTOR = "LITEMODTUTOR";
- private ChannelBinding.RawDataChannel channel;
- private void sendData(Task task)
- {
- for (Player player : Sponge.getServer().getOnlinePlayers())
- {
- this.channel.sendTo(player, buf ->
- {
- String text = String.format("To %s: 2333", player.getName());
- buf.writeByteArray(text.getBytes(Charsets.UTF_8));
- });
- }
- }
- @Inject
- private Logger logger;
- @Listener
- public void onServerStarted(GameStartedServerEvent event)
- {
- this.channel = Sponge.getChannelRegistrar().createRawChannel(this, CHANNEL_LITEMODTUTOR);
- Sponge.getScheduler().createTaskBuilder().intervalTicks(25).execute(this::sendData).submit(this);
- this.logger.info("Plugin enabled. ");
- }
- }
复制代码 我们以平均25tick每次的频率向客户端发送玩家名称和2333的弹幕,然后在客户端显示。 如果不出所料,显示的内容应该是这样子的:
 虽然这个“弹幕”的功能很不完美,不过作为教程的示例应该是够用的。 |
配置文件:
现在我们先解决一下在教程前面提到的问题。 LiteLoader提供两种类型的配置文件:一种为版本通用的配置文件,默认存放在“liteconfig/common”文件夹,一种为随不同版本而变的配置文件,默认存放在“liteconfig/config.{Minecraft版本}”文件夹。
那么我们该如何声明我们的Mod到底使用的是哪种配置文件类型呢?配置文件又应该叫作什么名字呢?我们在我们的Mod主类开头增加一个注解:
- /**
- * @author ustc_zzzz
- */
- @ExposableOptions(strategy = ConfigStrategy.Versioned, filename = "litemodtutor.json")
- public class LiteModTutor implements Tickable, PluginChannelListener
复制代码 这个注解的“strategy”参数表示配置文件是通用的还是随版本而变的,而“filename”参数表示相应文件夹下的名字。这个注解其实还有一个名为“aggressive”的参数,默认为false,如果设置为true,那么保存配置时如果配置文件里已有该项配置,那么相关字段的值将覆盖已有配置,否则不予替换。
如果读者有认真分析之前我提供的游戏逻辑的话,应该会注意到有一个量“movePerTick”被我单独提成了字段。现在我们如果想要配置它的话,LiteLoader为我们提供了一种非常简单的方法:在相应的字段上加注解:
- @Expose
- @SerializedName("move_per_tick")
- private float movePerTick = 5.0F;
复制代码 “@Expose”注解表明这个字段加入配置文件中,而“@SerializedName”注解表明这个字段到底该是什么名字。
现在我们运行一次游戏,在“liteconfig/config.1.11.2”文件夹下应该有一个名为“litemodtutor.json”的文件,这个文件的内容应该是这样子的:
很好,我们把一个字段和配置文件中的一行完成了关联。 |
字节码的操纵:
读者问到这里可能会想:LiteLoader目前提供的功能,我用Forge Mod都可以做到啊?没错,但LiteLoader的魅力还不止这些。其中最引人注目的特性之一就是LiteLoader自Minecraft 1.8.9开始提供的一套名为Mixin的框架,这套框架可以非常方便地以hook的方式操纵低层字节码。由于Mixin的内容比较多,作者就不在这里讲述,而会开单独的一篇文章讲解。使用Mixin操纵字节码会变得非常方便,甚至不需要了解Java的字节码就可以使用Mixin。
教程相关代码:
这里提供了本篇教程中使用的所有相关源代码,它们已经以ZIP形式打包好:
最后,感谢所有读者对本人的教程提供的支持。谢谢大家!
来自群组: InfinityStudio |
评分查看全部评分
|