诸如冰与火之歌、深渊国度等模组,玩家往往可以通过使用模组中的参考书籍来达到快速上手的效果。
冰与火之歌异兽手记
我们今天在模组中实现一本模组参考书籍,方便其他玩家游玩我们的模组。
1.在Java包的items包中新建一个itemBook包,用于存放我们的书籍代码 -> 在itemBook包中新建elements包 -> 在elements包中新建Element类
Element.java
package com.joy187.re8joymod.common.items.itemBook.elements;
public abstract class Element
{
public int position = 0;
public int x = 0;
public int y = 0;
public int w = 0;
public int h = 0;
public int z = 0;
@Override
public String toString()
{
return toString(false);
}
public abstract String toString(boolean complete);
}
在elements包中新建接口类ElementFactory
,后续要使用到这个接口
ElementFactory.java
package com.joy187.re8joymod.common.items.itemBook.elements;
@FunctionalInterface
public interface ElementFactory
{
Element newInstance();
}
在elements包中新建接口类ParsingContext
ParsingContext.java
package com.joy187.re8joymod.common.items.itemBook.elements;
import javax.xml.parsers.DocumentBuilder;
public interface ParsingContext
{
boolean loadedFromConfigFolder();
DocumentBuilder xmlDocumentBuilder();
}
2.在itemBook包中新建ItemStoryBook
类,作为我们书籍的物品类
ItemStoryBook.java
package com.joy187.re8joymod.common.items.itemBook;
import net.minecraft.client.Minecraft;
import net.minecraft.client.util.ITooltipFlag;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.ItemUseContext;
import net.minecraft.nbt.CompoundNBT;
import net.minecraft.util.ActionResult;
import net.minecraft.util.ActionResultType;
import net.minecraft.util.Hand;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.world.World;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.fml.loading.FMLEnvironment;
import javax.annotation.Nullable;
import java.util.List;
public class ItemStoryBook extends Item {
public ItemStoryBook(Properties properties) {
super(properties);
}
@Override
public ActionResultType useOn(ItemUseContext context) {
return showBook(context.getLevel(), context.getItemInHand()).getResult();
}
//右键打开我们的书本
@Override
public ActionResult<ItemStack> use(World worldIn, PlayerEntity playerIn, Hand hand) {
//System.out.println("OPEN");
ItemStack stack = playerIn.getItemInHand(hand);
return showBook(worldIn, stack);
}
//展示书籍页面
private ActionResult<ItemStack> showBook(World worldIn, ItemStack stack) {
if (!worldIn.isClientSide)
return ActionResult.success(stack);
CompoundNBT nbt = stack.getTag();
//Minecraft.getInstance().setScreen(new GuidebookScreen(new ResourceLocation(Main.MOD_ID, "textures/gui/re8storybookicon.png")));
if (FMLEnvironment.dist == Dist.CLIENT)
Minecraft.getInstance().setScreen(new BookBase(stack));
return ActionResult.success(stack);
}
public ItemStack of(ResourceLocation book) {
ItemStack stack = new ItemStack(this);
CompoundNBT tag = new CompoundNBT();
tag.putString("Book", book.toString());
stack.setTag(tag);
return stack;
}
@Nullable
public String getBookLocation(ItemStack stack) {
CompoundNBT tag = stack.getTag();
if (tag != null) {
return tag.getString("Book");
}
return null;
}
@Override
public void appendHoverText(ItemStack stack, @Nullable World worldIn, List<ITextComponent> tooltip, ITooltipFlag flagIn) {
super.appendHoverText(stack, worldIn, tooltip, flagIn);
if (flagIn == ITooltipFlag.TooltipFlags.ADVANCED) {
String book = getBookLocation(stack);
}
}
@Override
public ITextComponent getName(ItemStack stack) {
String book = getBookLocation(stack);
return super.getName(stack);
}
public static String getSubtype(ItemStack stack) {
if (stack.getItem() instanceof ItemStoryBook) {
String bookLocation = ((ItemStoryBook) stack.getItem()).getBookLocation(stack);
return bookLocation == null ? "" : bookLocation;
}
return "";
}
}
3.在itemBook包中新建BookDocument
类,记录书中内容:
BookDocument.java
package com.joy187.re8joymod.common.items.itemBook;
import com.google.common.collect.Maps;
import com.google.common.primitives.Floats;
import com.joy187.re8joymod.common.items.itemBook.elements.ElementFactory;
import com.joy187.re8joymod.common.items.itemBook.elements.ParsingContext;
import net.minecraft.client.renderer.model.ModelResourceLocation;
import net.minecraft.util.ResourceLocation;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.InputStream;
import java.util.Map;
public class BookDocument
{
private static final float DEFAULT_FONT_SIZE = 1.0f;
private float fontSize = 1.0f;
public SectionRef home = new SectionRef(0, 0);
private final ResourceLocation bookLocation;
private String bookName;
private ResourceLocation bookCover;
private ResourceLocation bookModel;
final Map<String, Integer> chaptersByName = Maps.newHashMap();
private static final Map<ResourceLocation, ElementFactory> customElements = Maps.newHashMap();
private ResourceLocation background;
public static void registerCustomElement(ResourceLocation location, ElementFactory factory)
{
if (customElements.containsKey(location))
throw new RuntimeException("Can not register two custom element factories with the same id.");
customElements.put(location, factory);
}
public BookDocument(ResourceLocation bookLocation)
{
this.bookLocation = bookLocation;
}
public ResourceLocation getLocation()
{
return bookLocation;
}
public boolean parseBook(InputStream stream, boolean loadedFromConfigFolder)
{
try
{
//chapters.clear();
bookName = "";
bookCover = null;
fontSize = DEFAULT_FONT_SIZE;
//chaptersByName.clear();
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.newDocument();
ParsingContext parsingContext = new ParsingContext()
{
@Override
public boolean loadedFromConfigFolder()
{
return loadedFromConfigFolder;
}
@Override
public DocumentBuilder xmlDocumentBuilder()
{
return dBuilder;
}
};
doc.getDocumentElement().normalize();
Node root = doc.getChildNodes().item(0);
if (root.hasAttributes())
{
NamedNodeMap attributes = root.getAttributes();
Node n = attributes.getNamedItem("title");
if (n != null)
{
bookName = n.getTextContent();
}
n = attributes.getNamedItem("cover");
if (n != null)
{
bookCover = new ResourceLocation(n.getTextContent());
}
n = attributes.getNamedItem("model");
if (n != null)
{
String text = n.getTextContent();
if (text.contains("#"))
bookModel = new ModelResourceLocation(text);
else
bookModel = new ResourceLocation(text);
}
n = attributes.getNamedItem("background");
if (n != null)
{
background = new ResourceLocation(n.getTextContent());
}
n = attributes.getNamedItem("fontSize");
if (n != null)
{
Float f = Floats.tryParse(n.getTextContent());
fontSize = f != null ? f : DEFAULT_FONT_SIZE;
}
n = attributes.getNamedItem("home");
if (n != null)
{
String ref = n.getTextContent();
home = SectionRef.fromString(ref);
}
n = attributes.getNamedItem("dependencies");
if (n != null)
{
for (String s : n.getTextContent().split(","))
{
// TODO
/*if (!Loader.isModLoaded(s))
{
initializeWithLoadError("Dependency not loaded: " + s);
return false;
}*/
}
}
}
parseDocumentLevelElements(parsingContext, root.getChildNodes());
}
catch (ParserConfigurationException e)
{
//initializeWithLoadError(e.toString());
}
return true;
}
// private static final Map<ResourceLocation, Document> includeCache = new HashMap<>();
//
private void parseDocumentLevelElements(ParsingContext context, NodeList firstLevel)
{
int chapterNumber = 0;
for (int i = 0; i < firstLevel.getLength(); i++)
{
Node firstLevelNode = firstLevel.item(i);
//chapterNumber = parseDocumentLevelElement(context, chapterNumber, firstLevelNode);
}
}
}
3.在itemBook包中新建ChangePageButton
类,用来实现我们的页面跳转:
ChangePageButton.java
package com.joy187.re8joymod.common.items.itemBook;
import com.joy187.re8joymod.Utils;
import net.minecraft.client.gui.widget.button.Button;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.text.TranslationTextComponent;
public class ChangePageButton extends Button {
private final boolean right;
private int color;
public ChangePageButton(int x, int y, boolean right, int color, Button.IPressable press) {
super(x, y, 23, 10, new TranslationTextComponent(""), press);
this.right = right;
this.color = color;
}
}
同时在包中新建SectionRef
类:
SectionRef.java
package com.joy187.re8joymod.common.items.itemBook;
import com.google.common.base.Strings;
import com.google.common.primitives.Ints;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class SectionRef
{
public int chapter;
public int section;
public boolean resolvedNames = false;
public String chapterName;
public String sectionName;
public SectionRef(int chapter, int section)
{
this.chapter = chapter;
this.section = section;
resolvedNames = true;
}
public SectionRef(String chapter, @Nullable String sectionName)
{
this.chapterName = chapter;
this.sectionName = sectionName;
}
/**
* @param bookDocument the book which contains the referenced section
* @return <code>false</code> if the {@link SectionRef} has no valid target
*/
public boolean resolve(BookDocument bookDocument)
{
if (!resolvedNames)
{
try
{
if (!Strings.isNullOrEmpty(chapterName))
{
Integer ch = Ints.tryParse(chapterName);
if (ch != null)
{
chapter = ch;
}
else
{
chapter = bookDocument.chaptersByName.get(chapterName);
}
if (!Strings.isNullOrEmpty(sectionName))
{
Integer pg = Ints.tryParse(sectionName);
if (pg != null)
{
section = pg;
}
}
}
else
{
//throw error if neither field is defined
throw new InvalidPageRefException("Invalid format: missing section and chapter");
}
}
catch (Exception e)
{
//try to parse the section ref into a string: <chapter>:<section>
String ref_string = (Strings.isNullOrEmpty(chapterName) ? "<none>" : (chapterName)) +
":" + (Strings.isNullOrEmpty(sectionName) ? "<none>" : (sectionName));
//log error
System.out.print("error");
return false; // =section ref has no valid target
}
}
return true;
}
public SectionRef copy()
{
return new SectionRef(chapter, section);
}
/**
* Thrown by {@link SectionRef#resolve(BookDocument)} in any case that normally wouldn't cause an exception
* but still signifies that the {@link SectionRef} has no valid target and is therefore invalid.
*/
public static class InvalidPageRefException extends Exception
{
public InvalidPageRefException(String s)
{
super(s);
}
}
/**
* Parses a String into a {@link SectionRef}.
*
* @param refString the string to be parsed
*/
public static SectionRef fromString(@Nonnull String refString)
{
if (refString.indexOf(':') >= 0)
{
String[] parts = refString.split(":");
return new SectionRef(parts[0], parts[1]);
}
else
{
return new SectionRef(refString, null);
}
}
@Override
public boolean equals(Object obj)
{
if (!(obj instanceof SectionRef))
return false;
SectionRef pr = (SectionRef) obj;
return resolvedNames && pr.resolvedNames && pr.chapter == chapter && pr.section == section;
}
@Override
public int hashCode()
{
if (!resolvedNames)
return 0;
return chapter * 313 + section;
}
}
4.在itemBook包中新建BookBase
类,对我们书籍页面的所有内容进行编写:
BookBase.java
package com.joy187.re8joymod.common.items.itemBook;
import com.google.common.collect.Maps;
import com.joy187.re8joymod.Utils;
import com.joy187.re8joymod.common.init.ModBlocks;
import com.mojang.blaze3d.matrix.MatrixStack;
import com.mojang.blaze3d.systems.RenderSystem;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.resources.I18n;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.text.TranslationTextComponent;
import org.lwjgl.opengl.GL11;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class BookBase extends Screen {
//这个是我们背景图片的长宽
protected static final int X = 256;
protected static final int Y = 256;
int page = 0;
//我们书籍背景图片的地址,一般都存在gui文件夹下
private static final ResourceLocation TEXTURE=
new ResourceLocation(Utils.MOD_ID, "textures/gui/re8storybookicon.png");
//我们书籍插图的地址
private static final ResourceLocation DIMI=
new ResourceLocation(Utils.MOD_ID, "textures/gui/dimi.png");
//private static final ResourceLocation MO=
// new ResourceLocation(Utils.MOD_ID, "textures/gui/mo.png");
//public static final BookBase BOOK_BASE = new Page1();
private static final Map<String, ResourceLocation> PICTURE_LOCATION_CACHE = Maps.newHashMap();
//两个按键,一个往前一页,一个往后一页
public ChangePageButton previousPage;
public ChangePageButton nextPage;
//我们当前在第几页
public int bookPages;
//一共多少页
public int bookPagesTotal = 10;
protected ItemStack book;
protected boolean index;
int pageType;
public BookBase()
{
super(new TranslationTextComponent(""));
index = true;
}
public BookBase(ItemStack book) {
super(new TranslationTextComponent(""));
this.book = book;
index = true;
}
//将书本内容显示在屏幕上
public void render(MatrixStack pMatrixStack, int mouseX, int mouseY, float delta) {
renderBackground(pMatrixStack);
RenderSystem.bindTexture(0);
this.renderBackground(pMatrixStack);
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
//显示背景图片
this.getMinecraft().getTextureManager().bind(TEXTURE);
this.getMinecraft().getTextureManager().getTexture(TEXTURE);
//作为我们的坐标基准对整个书籍进行渲染
int cornerX = (this.width - X) / 2;
int cornerY = (this.height - Y) / 2;
blit(pMatrixStack, cornerX, cornerY, 0, 0, X, Y, 256, 256);
if (this.bookPages>=0) {
//显示每张纸的内容
drawPerPage(pMatrixStack, this.bookPages);
int pageLeft = bookPages * 2 + 1;
int pageRight = pageLeft + 1;
// font.draw(pMatrixStack, "" + pageLeft, book_left,book_top, 0X303030);
// font.draw(pMatrixStack, "" + pageRight, book_left,book_top, 0X303030);
font.draw(pMatrixStack, "" + pageLeft, cornerX, cornerY - (int) (Y * 0.13), 0X303030);
font.draw(pMatrixStack, "" + pageRight, cornerX, cornerY - (int) (Y * 0.13), 0X303030);
}
super.render(pMatrixStack, mouseX, mouseY, delta);
//System.out.println("当前页"+this.bookPages+".");
}
//初始化两个按钮
@Override
public void init()
{
super.init();
//设置坐标基准
int cornerX = (this.width - X) / 2;
int cornerY = (this.height - Y) / 2+220;
//往前翻页的按钮
this.addButton(
this.previousPage = new ChangePageButton(cornerX+20, cornerY, false, 0, (p_238834_1_) -> {
//如果没有到第一页,我们就一直往前翻页
if(this.bookPages>0)
this.bookPages--;
else
this.bookPages=0;
}));
//添加往后翻页按钮
this.addButton(
this.nextPage = new ChangePageButton(cornerX+220, cornerY, true, 0, (p_214132_1_) -> {
//如果没有到最后一页,我们就一直往后翻页
if(this.bookPages<this.bookPagesTotal)
this.bookPages++;
}));
//你可以设置其他按钮
//...
}
//将每张页面描绘出来
public void drawPerPage(MatrixStack ms, int bookPages) {
switch (this.bookPages) {
case 0:
if (bookPages == 0) {
//System.out.println("第一页");
ms.pushPose();
//乘上放大倍数(x放大几倍,y放大几倍,z放大几倍),都是1说明字体不放大
ms.scale(1F, 1F, 1F);
//这是我们第一页要显示的字符串,在语言文件中对应"book.welcome"的字段
String text = I18n.get("book.welcome");
int cornerX = (this.width - X) / 2 +10;
int cornerY = (this.height - Y) / 2+10;
//在哪里把这些文字写出来,参数:渲染栈、文字、x坐标、y坐标、字体颜色
font.draw(ms,text,cornerX,cornerY+10, 0X303030);
ms.popPose();
}
break;
case 1:
if (bookPages == 1) {
ms.pushPose();
ms.scale(1F, 1F, 1F);
//我们第二页显示的文字
String text = I18n.get("book.enterdimension");
int cornerX = (this.width - X) / 2 +10;
int cornerY = (this.height - Y) / 2+15;
int centerY = cornerY;
int len = text.length();
//我们设定一行显示24个字,到头就换行
for(int i=0;i*24<len;i++)
{
String temp ="";
if(i*24+24<len){
temp=text.substring(i*24,(i+1)*24);
}
else{
temp=text.substring(i*24);
}
centerY=cornerY+10*i;
font.draw(ms,temp,cornerX,cornerY+10*i, 0x000000);
}
//font.draw(ms,text,cornerX,cornerY+10, 0x000000);
centerY +=10;
//我们将方块、物品显示在书籍上面,首先要得到物品
ItemStack item = new ItemStack(ModBlocks.DHANDS_BLOCK.get());
//将物品放在书上面,参数:物品,x坐标,y坐标
this.itemRenderer.renderGuiItem(item, cornerX, centerY);
this.itemRenderer.renderGuiItem(item, cornerX+10, centerY+5);
this.itemRenderer.renderGuiItem(item, cornerX+20, centerY+10);
this.itemRenderer.renderGuiItem(item, cornerX+30, centerY+15);
this.itemRenderer.renderGuiItem(item, cornerX, centerY+12);
this.itemRenderer.renderGuiItem(item, cornerX+30, centerY+15+12);
this.itemRenderer.renderGuiItem(item, cornerX, centerY+24);
this.itemRenderer.renderGuiItem(item, cornerX+30, centerY+15+24);
this.itemRenderer.renderGuiItem(item, cornerX, centerY+36);
this.itemRenderer.renderGuiItem(item, cornerX+30, centerY+15+36);
this.itemRenderer.renderGuiItem(item, cornerX, centerY+48);
this.itemRenderer.renderGuiItem(item, cornerX+10, centerY+5+48);
this.itemRenderer.renderGuiItem(item, cornerX+20, centerY+10+48);
this.itemRenderer.renderGuiItem(item, cornerX+30, centerY+15+48);
ItemStack item2 = new ItemStack(Items.WATER_BUCKET);
this.itemRenderer.renderGuiItem(item2, cornerX+50, centerY+15+24);
ms.popPose();
}
break;
case 2:
if (bookPages == 2) {
//我们在第本页将一张我们的插图显示出来
int cornerX = (this.width - X) / 2 +15;
int cornerY = (this.height - Y) / 2+45;
drawImage(ms, DIMI, cornerX, cornerY, 0, 0, 101, 128, 512F);
}
default:
break;
}
}
@Override
public void tick()
{
super.tick();
// if (background.update())
// minecraft.setScreen(null);
//updateButtonStates();
}
@Override
public boolean isPauseScreen()
{
return true;
}
//放入插图的函数,你只需要改动x,y,width,height这几个参数值
public void drawImage(MatrixStack ms, ResourceLocation texture, int x, int y, int u, int v, int width, int height, float scale) {
ms.pushPose();
this.getMinecraft().getTextureManager().bind(texture);
//图片放大多少倍
ms.scale(scale / 512F, scale / 512F, scale / 512F);
blit(ms, x, y, u, v, width, height, width, height);
ms.popPose();
}
}
5.在ModItems中将我们的书本进行注册:
public static final RegistryObject<Item> STORYBOOK = ITEMS.register("storybook",
() -> new ItemStoryBook(new Item.Properties().tab(RegistryEvents.RE8GROUP)));
6.代码部分结束,进入资源包制作阶段,在resources\assets\你的modid\models\item
中新建书籍的模型文件你可以使用2D模型,也可以用3D模型:
2D模型
storybook.json
{
"parent": "item/generated",
"textures": {
"layer0": "re8joymod:items/storybook"
}
}
3D模型(Blockbench导出)记得把textures换成你的贴图
storybook.json
{
"credit": "Made with Blockbench",
"parent": "",
"textures": {
"3": "re8joymod:items/book",
"particle": "re8joymod:items/m1897"
},
"elements": [
{
"from": [8.5, 2, 4.0412],
"to": [9.5, 9, 14.2412],
"rotation": {"angle": 0, "axis": "y", "origin": [9, 3.25826, 10.5412]},
"faces": {
"north": {"uv": [4, 1.25, 7.75, 2], "texture": "#3"},
"east": {"uv": [11.75, 3, 15.75, 3.75], "texture": "#3"},
"south": {"uv": [9.5, 0.5, 13, 1.5], "texture": "#3"},
"west": {"uv": [0, 0, 16, 16], "texture": "#3"},
"up": {"uv": [7.25, 15.25, 11.75, 15.75], "rotation": 270, "texture": "#3"},
"down": {"uv": [7.25, 15.25, 12, 15.75], "rotation": 90, "texture": "#3"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [21.31, 77.36, -10.98],
"translation": [0.25, 5, 1.75]
},
"thirdperson_lefthand": {
"rotation": [10.86, 48.24, -18.21],
"translation": [2.5, 5.75, -1]
},
"firstperson_righthand": {
"rotation": [8.55, 69.56, -11.85],
"translation": [-3.25, 6.75, 1.5]
},
"firstperson_lefthand": {
"rotation": [-180, -80.5, 179],
"translation": [-1.25, 7.25, 3]
},
"ground": {
"rotation": [-179.62, 30.1, 91.44],
"translation": [-1.75, 2.5, 0.25]
},
"gui": {
"rotation": [14.04, 88.98, 17.47],
"translation": [-1.5, 1.5, 0]
},
"head": {
"rotation": [0, -88.75, 0],
"translation": [1.5, 12.5, -1.75]
},
"fixed": {
"rotation": [167.12, -89.09, 164.6],
"translation": [2.75, 5, -3.25],
"scale": [2, 2, 2]
}
},
"groups": [
{
"name": "book",
"origin": [8, 8, 8],
"color": 0,
"children": [0]
}
]
}
在textures\items
中放入我们书籍物品的贴图:
在textures\gui
中放入我们书籍物品的背景图片(与第四步中的保持一致,大小256×256,文末可下载)、插图:
在lang\en_us.json
中将我们书籍的名称以及第四步中的书籍中的所有英文文字进行自定义
en_us.json
"item.re8joymod.storybook":"Story Book",
"book.welcome":"Welcome to the Resident Evil 8 World!",
"book.enterdimension":"Before entering the Village of Shadow,you need to use Ferocious Totems to create a portal,then use Water Bucket to activate it:",
在lang\en_us.json
中将我们书籍的名称以及第四步中的书籍中的所有中文文字进行自定义
zh_cn.json
"item.re8joymod.storybook":"故事书",
"book.welcome":"欢迎来到生化8的世界!",
"book.enterdimension":"当你进入这个神秘的世界之前,你需要使用图腾柱搭建一个传送门框架,之后用水桶将其激活:",
7.保存所有文件,刷新项目 -> 启动游戏调试
我们拿出书籍,右键就会进入首页:
之后按右下角的按钮,进入下一页,查看我们的文字和物品是否都正确摆放
再往后翻一页,查看我们的插图是否正常显示
tqlOrz
太强了啊
MC?
你才发现啊
tql!