├── .gitignore
├── src
├── entity
│ ├── intro.md
│ ├── scratchentity.assets
│ │ ├── image-20200503201245172.png
│ │ ├── image-20200503201357602.png
│ │ └── An-illustration-of-the-three-angles-yaw-pitch-and-roll-returned-by-the-Hyper-IMU.png
│ ├── passiveentityandai.assets
│ │ └── image-20200503220203980.png
│ └── passiveentityandai.md
├── potion
│ └── intro.md
├── networking
│ ├── intro.md
│ ├── secure.md
│ ├── custompack.assets
│ │ └── image-20200501102439453.png
│ └── custompack.md
├── attributes
│ └── intro.md
├── coremode
│ └── intro.md
├── specialmodel
│ ├── animation.md
│ ├── b3d.md
│ ├── intro.md
│ ├── obj.assets
│ │ ├── obsidian_obj.png
│ │ ├── image-20200429095433074.png
│ │ ├── image-20200724230101066.png
│ │ ├── image-20200724230406389.png
│ │ ├── obsidian_obj.mtl
│ │ └── obsidian_obj.obj
│ └── obj.md
├── compatible
│ └── intro.md
├── datagnerator
│ └── intro.md
├── gui
│ ├── intro.md
│ ├── container.assets
│ │ ├── Inventoryandslot.jpg
│ │ └── image-20200508193516742.png
│ ├── hud.assets
│ │ └── image-20200509082639854.png
│ ├── firstgui.assets
│ │ ├── image-20200501210032253.png
│ │ ├── image-20200501210423881.png
│ │ ├── image-20200502103302036.png
│ │ └── ABDAB8F2-7777-4249-9249-2278B64FF5BA.jpeg
│ ├── hud.md
│ └── firstgui.md
├── specialrender
│ ├── intro.md
│ ├── ter.assets
│ │ └── image-20200509093429490.png
│ ├── ibakedmodel.assets
│ │ └── image-20200430173122813.png
│ ├── ter.md
│ └── ister.md
├── item
│ ├── intro.md
│ ├── food.assets
│ │ ├── obsidian_apple.png
│ │ └── image-20200427165949417.png
│ ├── meleeweapons.assets
│ │ ├── obsidian_sword.png
│ │ ├── image-20200427182723660.png
│ │ └── image-20200427184918516.png
│ ├── group.assets
│ │ └── image-20200427211358242.png
│ ├── modelandtextures.assets
│ │ ├── obsidian_ingot.png
│ │ └── image-20200427113433338.png
│ ├── firstitem.assets
│ │ ├── image-20200427101846434.png
│ │ ├── image-20200427101903271.png
│ │ └── image-20200427102030250.png
│ ├── itemstack.assets
│ │ └── image-20200428080226198.png
│ ├── itemstack.md
│ ├── group.md
│ ├── modelandtextures.md
│ ├── food.md
│ ├── meleeweapons.md
│ └── firstitem.md
├── block
│ ├── intro.md
│ ├── nonesoildblock.assets
│ │ ├── obsidian_frame.png
│ │ ├── image-20200428204119348.png
│ │ └── image-20200428214022814.png
│ ├── rendertype.assets
│ │ ├── blockrenderlayers.png
│ │ └── image-20200625163139129.png
│ ├── modelandtextures.assets
│ │ ├── obsdian_block.png
│ │ ├── image-20200428181816541.png
│ │ └── image-20200428182406212.png
│ ├── firstblock.assets
│ │ ├── image-20200428155404000.png
│ │ ├── image-20200428155435757.png
│ │ ├── image-20200428162256286.png
│ │ ├── image-20200428164024589.png
│ │ └── image-20200428164037088.png
│ ├── blocksstates.assets
│ │ ├── obsidian_rubik_cube_texture_0.png
│ │ ├── obsidian_rubik_cube_texture_1.png
│ │ ├── 7B934D2C-8596-49FA-A5FE-05709ABFFBF2.jpeg
│ │ └── D2627490-F744-4BD9-B06C-09FC7CCB166B.jpeg
│ ├── blockandblockstate.assets
│ │ ├── A31FDCB0-F8AC-43BC-BD70-801476111C97.jpeg
│ │ └── EDC215E2-DF26-4764-AE2D-2640C7184482.jpeg
│ ├── blockandblockstate.md
│ ├── modelandtextures.md
│ ├── rendertype.md
│ ├── firstblock.md
│ ├── blocksstates.md
│ └── nonesoildblock.md
├── capability
│ ├── intro.md
│ ├── capabilityfromscratch.assets
│ │ ├── image-20200504112815135.png
│ │ └── image-20200504112836268.png
│ ├── attachcapabilityprovider.assets
│ │ └── image-20200505102131455.png
│ ├── simpleusage.md
│ ├── capabilityfromscratch.md
│ └── attachcapabilityprovider.md
├── devenvironment
│ ├── intro.md
│ ├── setup.assets
│ │ ├── image-20200426182350327.png
│ │ ├── image-20200426183157784.png
│ │ ├── image-20200426184039732.png
│ │ ├── image-20200426190531596.png
│ │ ├── image-20200426190742676.png
│ │ ├── image-20200426190744381.png
│ │ ├── image-20200426192001511.png
│ │ ├── image-20200426192128820.png
│ │ ├── image-20200426192159954.png
│ │ ├── image-20200426192350734.png
│ │ ├── image-20200426192959581.png
│ │ ├── image-20200426193203851.png
│ │ ├── image-20200426193439272.png
│ │ ├── image-20200426193520833.png
│ │ ├── image-20200426193744538.png
│ │ ├── image-20200426193827968.png
│ │ ├── image-20200426195031987.png
│ │ ├── image-20200426195216989.png
│ │ ├── image-20200426195313225.png
│ │ ├── image-20200426200831167.png
│ │ ├── image-20200426201131698.png
│ │ └── image-20200510143006591.png
│ ├── modinfo.assets
│ │ ├── image-20200427074821172.png
│ │ ├── image-20200427074901407.png
│ │ ├── image-20200427075653910.png
│ │ ├── image-20200427075728100.png
│ │ ├── image-20200427075739020.png
│ │ ├── image-20200427082638163.png
│ │ ├── image-20200427082837751.png
│ │ ├── image-20200427084446741.png
│ │ └── image-20200514205243616.png
│ ├── folderintro.md
│ ├── modinfo.md
│ └── setup.md
├── advancements
│ └── intro.md
├── worldgeneration
│ ├── intro.assets
│ │ ├── world.png
│ │ ├── biome1.jpg
│ │ ├── biome2.jpg
│ │ ├── dimension.png
│ │ ├── chunkgenerator.png
│ │ ├── image-20200510171449763.png
│ │ └── image-20200510174915212.png
│ ├── oregeneration.assets
│ │ └── image-20200510194853386.png
│ ├── structuregeneration.assets
│ │ └── image-20200512174944649.png
│ ├── biomegenerationandworldtype.assets
│ │ ├── image-20200512183933026.png
│ │ └── image-20200512185020541.png
│ ├── dimensionenerationandchunkgeneratorandbiomeprovider.assets
│ │ └── image-20200512214423977.png
│ ├── intro.md
│ ├── oregeneration.md
│ ├── biomegenerationandworldtype.md
│ ├── structuregeneration.md
│ └── dimensionenerationandchunkgeneratorandbiomeprovider.md
├── at
│ ├── intro.assets
│ │ ├── image-20200619212440509.png
│ │ ├── image-20200619212602433.png
│ │ ├── image-20200620175141140.png
│ │ └── image-20200620175534512.png
│ └── intro.md
├── io
│ ├── intro.assets
│ │ ├── image-20200516170355856.png
│ │ └── image-20200516170445384.png
│ └── intro.md
├── i18n
│ ├── i18n.assets
│ │ ├── image-20200427213837119.png
│ │ └── image-20200427220407913.png
│ └── i18n.md
├── tileentity
│ ├── intro.md
│ ├── datasync.assets
│ │ └── image-20200429215140877.png
│ ├── firsttileentity.assets
│ │ └── image-20200429165945640.png
│ ├── itickabletileentity.assets
│ │ └── image-20200429190609373.png
│ ├── itickabletileentity.md
│ └── datasync.md
├── command
│ ├── command.assets
│ │ └── image-20200515151824363.png
│ └── command.md
├── fluid
│ ├── firstfluid.assets
│ │ ├── image-20200509191653391.png
│ │ └── image-20200721114031017.png
│ └── firstfluid.md
├── paticles
│ └── intro.assets
│ │ └── image-20200614202142227.png
├── configure
│ ├── configure.assets
│ │ ├── image-20200516101413181.png
│ │ └── image-20200516101637019.png
│ └── configure.md
├── introducation
│ ├── vanilla.assets
│ │ └── image-20200426110629794.png
│ ├── developmentmodel.assets
│ │ └── Untitled Diagram.png
│ ├── coreconception.md
│ ├── developmentmodel.md
│ ├── intro.md
│ ├── vanilla.md
│ └── forge.md
├── datapack
│ ├── intro.md
│ ├── lootable.md
│ └── recipes.md
├── event
│ └── intro.md
├── sounds
│ └── intro.md
├── SUMMARY.md
└── worldsaveddata
│ └── example.md
├── book.toml
├── README.md
├── .github
└── workflows
│ └── deploy.yaml
└── pic
└── GUi.drawio
/.gitignore:
--------------------------------------------------------------------------------
1 | book
2 |
--------------------------------------------------------------------------------
/src/entity/intro.md:
--------------------------------------------------------------------------------
1 | # 实体
2 |
--------------------------------------------------------------------------------
/src/potion/intro.md:
--------------------------------------------------------------------------------
1 | # 药水
2 |
--------------------------------------------------------------------------------
/src/networking/intro.md:
--------------------------------------------------------------------------------
1 | # 网络包
2 |
--------------------------------------------------------------------------------
/src/attributes/intro.md:
--------------------------------------------------------------------------------
1 | # 属性系统
2 |
--------------------------------------------------------------------------------
/src/coremode/intro.md:
--------------------------------------------------------------------------------
1 | # CoreMod
2 |
--------------------------------------------------------------------------------
/src/networking/secure.md:
--------------------------------------------------------------------------------
1 | # 关于Mod安全
2 |
--------------------------------------------------------------------------------
/src/specialmodel/animation.md:
--------------------------------------------------------------------------------
1 | # 动画
2 |
--------------------------------------------------------------------------------
/src/compatible/intro.md:
--------------------------------------------------------------------------------
1 | # 与其他mod的兼容
2 |
--------------------------------------------------------------------------------
/src/datagnerator/intro.md:
--------------------------------------------------------------------------------
1 | # Data Generator
2 |
--------------------------------------------------------------------------------
/src/gui/intro.md:
--------------------------------------------------------------------------------
1 | # Gui
2 |
3 | 在这节我们将进入很多人期待已久的章节,GUI(图形界面)。
--------------------------------------------------------------------------------
/src/specialmodel/b3d.md:
--------------------------------------------------------------------------------
1 | # B3D 模型
2 |
3 | 在1.15中,关于B3D的支持似乎暂时被注释掉了。
--------------------------------------------------------------------------------
/src/specialrender/intro.md:
--------------------------------------------------------------------------------
1 | # 特殊渲染
2 |
3 | 在这一节中,我们将来学习一些常见的特殊渲染。
--------------------------------------------------------------------------------
/src/item/intro.md:
--------------------------------------------------------------------------------
1 | # 物品
2 |
3 | 物品是Minecraft中的基本元素之一,在这一节中,我将介绍如何创建物品。
4 |
5 |
--------------------------------------------------------------------------------
/src/block/intro.md:
--------------------------------------------------------------------------------
1 | # 方块
2 |
3 | 我们在这节中终于要进入Minecraft中最为迷人的其中一部分:方块。当然我们会从最为简单的方块开始学起。
--------------------------------------------------------------------------------
/src/capability/intro.md:
--------------------------------------------------------------------------------
1 | # 能力系统
2 |
3 | 在这节中,我们将会介绍Forge Mod开发过程中可能最为抽象的一部分内容:Capability系统。
--------------------------------------------------------------------------------
/src/devenvironment/intro.md:
--------------------------------------------------------------------------------
1 | # 环境配置
2 |
3 | 在这一节中,我们会讲解如何配置Forge的开发环境。(~~在这一节会劝退大部分人~~)
4 |
5 |
--------------------------------------------------------------------------------
/src/specialmodel/intro.md:
--------------------------------------------------------------------------------
1 | # 特殊模型
2 |
3 | 在这一节中,我们将学习如何在Minecraft中使用特殊的物品模型,Forge为Minecraft添加了额外两种物品模型的支持:OBJ和B3D。
--------------------------------------------------------------------------------
/src/advancements/intro.md:
--------------------------------------------------------------------------------
1 | # 进度
2 |
3 | 关于进度可以参照原版数据包的[教程](https://zhangshenxing.gitee.io/vanillamodtutorial/#2.4_进度)。
4 |
5 |
--------------------------------------------------------------------------------
/src/item/food.assets/obsidian_apple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/item/food.assets/obsidian_apple.png
--------------------------------------------------------------------------------
/src/worldgeneration/intro.assets/world.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/worldgeneration/intro.assets/world.png
--------------------------------------------------------------------------------
/src/gui/container.assets/Inventoryandslot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/gui/container.assets/Inventoryandslot.jpg
--------------------------------------------------------------------------------
/src/specialmodel/obj.assets/obsidian_obj.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/specialmodel/obj.assets/obsidian_obj.png
--------------------------------------------------------------------------------
/src/worldgeneration/intro.assets/biome1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/worldgeneration/intro.assets/biome1.jpg
--------------------------------------------------------------------------------
/src/worldgeneration/intro.assets/biome2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/worldgeneration/intro.assets/biome2.jpg
--------------------------------------------------------------------------------
/src/at/intro.assets/image-20200619212440509.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/at/intro.assets/image-20200619212440509.png
--------------------------------------------------------------------------------
/src/at/intro.assets/image-20200619212602433.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/at/intro.assets/image-20200619212602433.png
--------------------------------------------------------------------------------
/src/at/intro.assets/image-20200620175141140.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/at/intro.assets/image-20200620175141140.png
--------------------------------------------------------------------------------
/src/at/intro.assets/image-20200620175534512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/at/intro.assets/image-20200620175534512.png
--------------------------------------------------------------------------------
/src/gui/hud.assets/image-20200509082639854.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/gui/hud.assets/image-20200509082639854.png
--------------------------------------------------------------------------------
/src/io/intro.assets/image-20200516170355856.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/io/intro.assets/image-20200516170355856.png
--------------------------------------------------------------------------------
/src/io/intro.assets/image-20200516170445384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/io/intro.assets/image-20200516170445384.png
--------------------------------------------------------------------------------
/src/item/meleeweapons.assets/obsidian_sword.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/item/meleeweapons.assets/obsidian_sword.png
--------------------------------------------------------------------------------
/src/worldgeneration/intro.assets/dimension.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/worldgeneration/intro.assets/dimension.png
--------------------------------------------------------------------------------
/src/block/nonesoildblock.assets/obsidian_frame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/nonesoildblock.assets/obsidian_frame.png
--------------------------------------------------------------------------------
/src/block/rendertype.assets/blockrenderlayers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/rendertype.assets/blockrenderlayers.png
--------------------------------------------------------------------------------
/src/i18n/i18n.assets/image-20200427213837119.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/i18n/i18n.assets/image-20200427213837119.png
--------------------------------------------------------------------------------
/src/i18n/i18n.assets/image-20200427220407913.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/i18n/i18n.assets/image-20200427220407913.png
--------------------------------------------------------------------------------
/src/item/food.assets/image-20200427165949417.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/item/food.assets/image-20200427165949417.png
--------------------------------------------------------------------------------
/src/item/group.assets/image-20200427211358242.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/item/group.assets/image-20200427211358242.png
--------------------------------------------------------------------------------
/src/block/modelandtextures.assets/obsdian_block.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/modelandtextures.assets/obsdian_block.png
--------------------------------------------------------------------------------
/src/gui/container.assets/image-20200508193516742.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/gui/container.assets/image-20200508193516742.png
--------------------------------------------------------------------------------
/src/gui/firstgui.assets/image-20200501210032253.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/gui/firstgui.assets/image-20200501210032253.png
--------------------------------------------------------------------------------
/src/gui/firstgui.assets/image-20200501210423881.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/gui/firstgui.assets/image-20200501210423881.png
--------------------------------------------------------------------------------
/src/gui/firstgui.assets/image-20200502103302036.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/gui/firstgui.assets/image-20200502103302036.png
--------------------------------------------------------------------------------
/src/item/modelandtextures.assets/obsidian_ingot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/item/modelandtextures.assets/obsidian_ingot.png
--------------------------------------------------------------------------------
/src/tileentity/intro.md:
--------------------------------------------------------------------------------
1 | # 方块实体
2 |
3 | 欢迎来到方块实体这一章,方块实体(BlockEntity或者TileEntity)是Minecraft中最为迷人的主题之一,原版的熔炉,工业模组里的机器,这些都是通过方块实体实现的,在这节中,我们将要学习如何创建我们自己的方块实体。
--------------------------------------------------------------------------------
/src/worldgeneration/intro.assets/chunkgenerator.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/worldgeneration/intro.assets/chunkgenerator.png
--------------------------------------------------------------------------------
/src/block/firstblock.assets/image-20200428155404000.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/firstblock.assets/image-20200428155404000.png
--------------------------------------------------------------------------------
/src/block/firstblock.assets/image-20200428155435757.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/firstblock.assets/image-20200428155435757.png
--------------------------------------------------------------------------------
/src/block/firstblock.assets/image-20200428162256286.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/firstblock.assets/image-20200428162256286.png
--------------------------------------------------------------------------------
/src/block/firstblock.assets/image-20200428164024589.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/firstblock.assets/image-20200428164024589.png
--------------------------------------------------------------------------------
/src/block/firstblock.assets/image-20200428164037088.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/firstblock.assets/image-20200428164037088.png
--------------------------------------------------------------------------------
/src/block/rendertype.assets/image-20200625163139129.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/rendertype.assets/image-20200625163139129.png
--------------------------------------------------------------------------------
/src/command/command.assets/image-20200515151824363.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/command/command.assets/image-20200515151824363.png
--------------------------------------------------------------------------------
/src/fluid/firstfluid.assets/image-20200509191653391.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/fluid/firstfluid.assets/image-20200509191653391.png
--------------------------------------------------------------------------------
/src/fluid/firstfluid.assets/image-20200721114031017.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/fluid/firstfluid.assets/image-20200721114031017.png
--------------------------------------------------------------------------------
/src/item/firstitem.assets/image-20200427101846434.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/item/firstitem.assets/image-20200427101846434.png
--------------------------------------------------------------------------------
/src/item/firstitem.assets/image-20200427101903271.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/item/firstitem.assets/image-20200427101903271.png
--------------------------------------------------------------------------------
/src/item/firstitem.assets/image-20200427102030250.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/item/firstitem.assets/image-20200427102030250.png
--------------------------------------------------------------------------------
/src/item/itemstack.assets/image-20200428080226198.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/item/itemstack.assets/image-20200428080226198.png
--------------------------------------------------------------------------------
/src/paticles/intro.assets/image-20200614202142227.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/paticles/intro.assets/image-20200614202142227.png
--------------------------------------------------------------------------------
/src/specialmodel/obj.assets/image-20200429095433074.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/specialmodel/obj.assets/image-20200429095433074.png
--------------------------------------------------------------------------------
/src/specialmodel/obj.assets/image-20200724230101066.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/specialmodel/obj.assets/image-20200724230101066.png
--------------------------------------------------------------------------------
/src/specialmodel/obj.assets/image-20200724230406389.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/specialmodel/obj.assets/image-20200724230406389.png
--------------------------------------------------------------------------------
/src/item/meleeweapons.assets/image-20200427182723660.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/item/meleeweapons.assets/image-20200427182723660.png
--------------------------------------------------------------------------------
/src/item/meleeweapons.assets/image-20200427184918516.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/item/meleeweapons.assets/image-20200427184918516.png
--------------------------------------------------------------------------------
/src/specialrender/ter.assets/image-20200509093429490.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/specialrender/ter.assets/image-20200509093429490.png
--------------------------------------------------------------------------------
/src/block/nonesoildblock.assets/image-20200428204119348.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/nonesoildblock.assets/image-20200428204119348.png
--------------------------------------------------------------------------------
/src/block/nonesoildblock.assets/image-20200428214022814.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/nonesoildblock.assets/image-20200428214022814.png
--------------------------------------------------------------------------------
/src/configure/configure.assets/image-20200516101413181.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/configure/configure.assets/image-20200516101413181.png
--------------------------------------------------------------------------------
/src/configure/configure.assets/image-20200516101637019.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/configure/configure.assets/image-20200516101637019.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426182350327.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426182350327.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426183157784.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426183157784.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426184039732.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426184039732.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426190531596.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426190531596.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426190742676.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426190742676.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426190744381.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426190744381.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426192001511.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426192001511.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426192128820.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426192128820.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426192159954.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426192159954.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426192350734.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426192350734.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426192959581.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426192959581.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426193203851.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426193203851.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426193439272.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426193439272.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426193520833.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426193520833.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426193744538.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426193744538.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426193827968.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426193827968.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426195031987.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426195031987.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426195216989.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426195216989.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426195313225.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426195313225.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426200831167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426200831167.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200426201131698.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200426201131698.png
--------------------------------------------------------------------------------
/src/devenvironment/setup.assets/image-20200510143006591.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/setup.assets/image-20200510143006591.png
--------------------------------------------------------------------------------
/src/entity/scratchentity.assets/image-20200503201245172.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/entity/scratchentity.assets/image-20200503201245172.png
--------------------------------------------------------------------------------
/src/entity/scratchentity.assets/image-20200503201357602.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/entity/scratchentity.assets/image-20200503201357602.png
--------------------------------------------------------------------------------
/src/introducation/vanilla.assets/image-20200426110629794.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/introducation/vanilla.assets/image-20200426110629794.png
--------------------------------------------------------------------------------
/src/item/modelandtextures.assets/image-20200427113433338.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/item/modelandtextures.assets/image-20200427113433338.png
--------------------------------------------------------------------------------
/src/networking/custompack.assets/image-20200501102439453.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/networking/custompack.assets/image-20200501102439453.png
--------------------------------------------------------------------------------
/src/tileentity/datasync.assets/image-20200429215140877.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/tileentity/datasync.assets/image-20200429215140877.png
--------------------------------------------------------------------------------
/src/worldgeneration/intro.assets/image-20200510171449763.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/worldgeneration/intro.assets/image-20200510171449763.png
--------------------------------------------------------------------------------
/src/worldgeneration/intro.assets/image-20200510174915212.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/worldgeneration/intro.assets/image-20200510174915212.png
--------------------------------------------------------------------------------
/src/block/modelandtextures.assets/image-20200428181816541.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/modelandtextures.assets/image-20200428181816541.png
--------------------------------------------------------------------------------
/src/block/modelandtextures.assets/image-20200428182406212.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/modelandtextures.assets/image-20200428182406212.png
--------------------------------------------------------------------------------
/src/devenvironment/modinfo.assets/image-20200427074821172.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/modinfo.assets/image-20200427074821172.png
--------------------------------------------------------------------------------
/src/devenvironment/modinfo.assets/image-20200427074901407.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/modinfo.assets/image-20200427074901407.png
--------------------------------------------------------------------------------
/src/devenvironment/modinfo.assets/image-20200427075653910.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/modinfo.assets/image-20200427075653910.png
--------------------------------------------------------------------------------
/src/devenvironment/modinfo.assets/image-20200427075728100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/modinfo.assets/image-20200427075728100.png
--------------------------------------------------------------------------------
/src/devenvironment/modinfo.assets/image-20200427075739020.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/modinfo.assets/image-20200427075739020.png
--------------------------------------------------------------------------------
/src/devenvironment/modinfo.assets/image-20200427082638163.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/modinfo.assets/image-20200427082638163.png
--------------------------------------------------------------------------------
/src/devenvironment/modinfo.assets/image-20200427082837751.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/modinfo.assets/image-20200427082837751.png
--------------------------------------------------------------------------------
/src/devenvironment/modinfo.assets/image-20200427084446741.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/modinfo.assets/image-20200427084446741.png
--------------------------------------------------------------------------------
/src/devenvironment/modinfo.assets/image-20200514205243616.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/devenvironment/modinfo.assets/image-20200514205243616.png
--------------------------------------------------------------------------------
/src/introducation/developmentmodel.assets/Untitled Diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/introducation/developmentmodel.assets/Untitled Diagram.png
--------------------------------------------------------------------------------
/src/block/blocksstates.assets/obsidian_rubik_cube_texture_0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/blocksstates.assets/obsidian_rubik_cube_texture_0.png
--------------------------------------------------------------------------------
/src/block/blocksstates.assets/obsidian_rubik_cube_texture_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/blocksstates.assets/obsidian_rubik_cube_texture_1.png
--------------------------------------------------------------------------------
/src/entity/passiveentityandai.assets/image-20200503220203980.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/entity/passiveentityandai.assets/image-20200503220203980.png
--------------------------------------------------------------------------------
/src/gui/firstgui.assets/ABDAB8F2-7777-4249-9249-2278B64FF5BA.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/gui/firstgui.assets/ABDAB8F2-7777-4249-9249-2278B64FF5BA.jpeg
--------------------------------------------------------------------------------
/src/specialrender/ibakedmodel.assets/image-20200430173122813.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/specialrender/ibakedmodel.assets/image-20200430173122813.png
--------------------------------------------------------------------------------
/src/tileentity/firsttileentity.assets/image-20200429165945640.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/tileentity/firsttileentity.assets/image-20200429165945640.png
--------------------------------------------------------------------------------
/src/datapack/intro.md:
--------------------------------------------------------------------------------
1 | # 数据包
2 |
3 | 在1.13之后,Mojang推出了Data Pack,将合成表凋落物等都改成了用json文件描述,作为Mod开发者的我们自然也可以利用这点,来编写我们自己的Data Pack。读者在阅读这章之前,请确保自己对Data Pack的制作有所了解,我在这里不会一一涉及每个配方,只会略微的提及要注意的点。
--------------------------------------------------------------------------------
/src/tileentity/itickabletileentity.assets/image-20200429190609373.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/tileentity/itickabletileentity.assets/image-20200429190609373.png
--------------------------------------------------------------------------------
/src/worldgeneration/oregeneration.assets/image-20200510194853386.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/worldgeneration/oregeneration.assets/image-20200510194853386.png
--------------------------------------------------------------------------------
/src/block/blocksstates.assets/7B934D2C-8596-49FA-A5FE-05709ABFFBF2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/blocksstates.assets/7B934D2C-8596-49FA-A5FE-05709ABFFBF2.jpeg
--------------------------------------------------------------------------------
/src/block/blocksstates.assets/D2627490-F744-4BD9-B06C-09FC7CCB166B.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/blocksstates.assets/D2627490-F744-4BD9-B06C-09FC7CCB166B.jpeg
--------------------------------------------------------------------------------
/src/capability/capabilityfromscratch.assets/image-20200504112815135.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/capability/capabilityfromscratch.assets/image-20200504112815135.png
--------------------------------------------------------------------------------
/src/capability/capabilityfromscratch.assets/image-20200504112836268.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/capability/capabilityfromscratch.assets/image-20200504112836268.png
--------------------------------------------------------------------------------
/src/capability/attachcapabilityprovider.assets/image-20200505102131455.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/capability/attachcapabilityprovider.assets/image-20200505102131455.png
--------------------------------------------------------------------------------
/src/worldgeneration/structuregeneration.assets/image-20200512174944649.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/worldgeneration/structuregeneration.assets/image-20200512174944649.png
--------------------------------------------------------------------------------
/src/block/blockandblockstate.assets/A31FDCB0-F8AC-43BC-BD70-801476111C97.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/blockandblockstate.assets/A31FDCB0-F8AC-43BC-BD70-801476111C97.jpeg
--------------------------------------------------------------------------------
/src/block/blockandblockstate.assets/EDC215E2-DF26-4764-AE2D-2640C7184482.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/block/blockandblockstate.assets/EDC215E2-DF26-4764-AE2D-2640C7184482.jpeg
--------------------------------------------------------------------------------
/src/worldgeneration/biomegenerationandworldtype.assets/image-20200512183933026.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/worldgeneration/biomegenerationandworldtype.assets/image-20200512183933026.png
--------------------------------------------------------------------------------
/src/worldgeneration/biomegenerationandworldtype.assets/image-20200512185020541.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/worldgeneration/biomegenerationandworldtype.assets/image-20200512185020541.png
--------------------------------------------------------------------------------
/book.toml:
--------------------------------------------------------------------------------
1 | [book]
2 | authors = ["FledgeXu"]
3 | language = "zh_cn"
4 | multilingual = false
5 | src = "src"
6 | title = "Minecraft Forge Mod Dev tutorial"
7 | [output.html]
8 | # mathjax-support = true
9 | google-analytics = "UA-80068655-2"
10 |
--------------------------------------------------------------------------------
/src/worldgeneration/dimensionenerationandchunkgeneratorandbiomeprovider.assets/image-20200512214423977.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/worldgeneration/dimensionenerationandchunkgeneratorandbiomeprovider.assets/image-20200512214423977.png
--------------------------------------------------------------------------------
/src/entity/scratchentity.assets/An-illustration-of-the-three-angles-yaw-pitch-and-roll-returned-by-the-Hyper-IMU.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FledgeXu/Neutrino/HEAD/src/entity/scratchentity.assets/An-illustration-of-the-three-angles-yaw-pitch-and-roll-returned-by-the-Hyper-IMU.png
--------------------------------------------------------------------------------
/src/specialmodel/obj.assets/obsidian_obj.mtl:
--------------------------------------------------------------------------------
1 | # Blender MTL File: 'tutorial.blend'
2 | # Material Count: 1
3 |
4 | newmtl Material
5 | Ns 323.999994
6 | Ka 1.000000 1.000000 1.000000
7 | Kd 0.800000 0.800000 0.800000
8 | Ks 0.500000 0.500000 0.500000
9 | Ke 0.000000 0.000000 0.000000
10 | Ni 1.450000
11 | d 1.000000
12 | illum 2
13 | map_Kd neutrino:block/obsidian_obj
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 许可证
2 | 
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
3 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yaml:
--------------------------------------------------------------------------------
1 | name: github pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-18.04
11 | steps:
12 | - uses: actions/checkout@v2
13 |
14 | - name: Setup mdBook
15 | uses: peaceiris/actions-mdbook@v1
16 | with:
17 | mdbook-version: 'latest'
18 |
19 | - run: mdbook build
20 |
21 | - name: Deploy
22 | uses: peaceiris/actions-gh-pages@v3
23 | with:
24 | github_token: ${{ secrets.GITHUB_TOKEN }}
25 | cname: neutrino.v2mcdev.com
26 | publish_dir: ./book
27 |
--------------------------------------------------------------------------------
/src/block/blockandblockstate.md:
--------------------------------------------------------------------------------
1 | # Block和BlockState
2 |
3 | 在开始我们接下来的讲解之前,我想先来讲一下`BlockState`这个概念,相信已经对`ItemStack`有所了解到你,应该也能很快理解这个概念。
4 |
5 | 就和在游戏中的所有物品其实是`Itemstack`一样,在游戏中的所有方块其实是`BlockState`,而`BlockState`相比起`Block`,还包括最为重要的`state`信息,也就是状态信息。状态信息乍一看很难以理解,但是其实非常好懂。我们以原版的栅栏举例,当你把原版的栅栏放在地上,栅栏会根据周围方块的不同自动地改变形状,这其实是栅栏自动地改变了状态,我们可以在F3调试模式下查看方块的状态。
6 |
7 | 
8 |
9 | 这是在默认方块状态下的栅栏模型。
10 |
11 | 
12 |
13 | 可以看到我们使用Debug Stick修改了栅栏的方块状态之后,方块的模型也相应地发生了改变。
14 |
15 | 对于一些简单方块来说,它们可能只有一种可能的状态,比如石头。
16 |
--------------------------------------------------------------------------------
/src/introducation/coreconception.md:
--------------------------------------------------------------------------------
1 | # 一些核心概念
2 |
3 | 在这一小节中,我会讲几个不难理解但是非常重要的概念。
4 |
5 | ## 注册
6 |
7 | 如果你想往Minecraft里添加一些内容,那么你必须做的一件事就是注册。注册是一种机制,告诉游戏本身,有哪东西可以使用。你注册时需要的东西基本上可以分成两个部分:一个注册名和一个实例。
8 |
9 | ## ResourceLocation
10 |
11 | 你可以把ResourceLocation想成一种特殊格式的字符串,它大概长成这样:`minecraft:textures/block/stone.png`,一个ResourceLocation指定了资源包下的一个特定的文件。举例来说,前面这个这个ResourceLocation代表了原版资源包下的石头的材质图片。ResouceLocation分成两部分,冒号前面的叫做「域(domain)」,在原版中只有一个域,即`minecraft`域,但是如果你开始开发mod,那么每个mod都会有一个或者多个域。冒号的后半部分是和`assets`文件夹内的目录结构一一对应的。从某种程度上来说,ResourceLocation就是一个特殊的URL。
12 |
13 | ## 模型和材质
14 |
15 | 在游戏中3d的对象基本上都有它的模型,模型和材质组合在一起规定了一个对象具体的样子。模型相当于是骨头,材质相当于是皮肤。在大部分时候,你的材质都是png图片,请注意保证你的材质背景是不透明的,其次不要在材质中使用半透明像素,会有不可预知的问题。
16 |
17 |
--------------------------------------------------------------------------------
/src/introducation/developmentmodel.md:
--------------------------------------------------------------------------------
1 | # 开发模型
2 |
3 | 在这节中,我们将会粗略的讲一讲Minecraft mod的开发模型是什么样子的,理解这个模型将有助于你理解mod开发中的很多操作是为了什么。
4 |
5 | 在我看来,Minecraft mod 开发基本上遵循了「事件驱动模式」,这里我们不会详细的讨论纠结什么是「事件驱动模式」,你只需要有一个感性的了解即可。
6 |
7 | 那么Minecraft「事件驱动模式」是怎么样子的呢?要回答这个问题,我们得先理清三个概念:「事件」「总线」和「事件处理器」。
8 |
9 | 首先什么是「事件」呢?就跟这个词表示的那样,「事件」就是「发生了某件事」。举例来说「当方块被破环」这个就是一个事件,「当玩家死亡」这个也是一个事件,当然我们前面举的都是非常具体的例子,事件也可以很抽象,比如「当渲染模型时」这个也是一个事件。
10 |
11 | 接下来什么是「事件处理器」呢?事件处理器就是用来处理「事件」的函数。我们可以创建一个事件处理器来处理「方块破坏事件」,里面的内容是「重新创建一个方块」,可以注册一个事件处理器来处理「玩家死亡事件」,里面的内容是「放置一个墓碑」。
12 |
13 | 最后是「总线」,总线是连接「事件」和「事件处理器」的工具,当「事件」发生的时候,「事件」的信息将会被发送到总线上,然后总线会选择监听了这个「事件」的「事件处理器」,执行这个事件处理器。
14 |
15 |
16 |
17 | 注意这张图里的事件和事件处理器是没有先后顺序的。
18 |
19 | 在Minecraft中,所写的逻辑基本上都是事件处理。
20 |
21 | 在Forge开发里有两条总线,`Mod总线`和`Forge总线`,所有和初始化相关的事件都是在`Mod总线`内,其他所有事件都在`Forge总线`内。
--------------------------------------------------------------------------------
/src/item/itemstack.md:
--------------------------------------------------------------------------------
1 | # Item和ItemStack
2 | 在这里,我想讲一下`Item`和`ItemStack`的区分。我们先从`ItemStack`开始一步一步思考为什么它们需要区分开。
3 |
4 | `ItemStack`顾名思义就是「物品堆」。实际上在游戏中,所有物品槽里放着的物品都是单独的`ItemStack`。
5 |
6 | 
7 |
8 | 比如在这种情况下,就有三个`ItemStack`。
9 |
10 | 但是这就引出了一个问题,虽然一组苹果和第二组苹果数量不同,但是这个数量其实并不影响他们的实际表现。它们同样可以被吃,吃了以后回复的效果也是相同的。
11 |
12 | 这些相当于「属性」或者「默认行为」是相同的,这些相同的逻辑就应该被抽出来,这就是`Item`。
13 |
14 | 还是以上图举例,这里就只有两种`Item`:苹果和铁剑。
15 |
16 | 你可以想象`ItemStack`就是`Item`的一个包装,它比起`Item`额外提供了数量,NBT标签等属性。
17 |
18 | 这里值得注意的是,`ItemStack`的数量为0,虽然代表是空了,这不代表它就变成`null`了,所以在你必须得用`ItemStack`下的`isEmpty()`方法来判断是否为空。
19 |
20 | `ItemStack`中所包含的`Item`其实是同一个实例,原因非常简单,如果不是同一个实例,会无意义地产生非常多相同的实例,出于优化的考虑,当然是共用一个实例合适,这同时意味着你可通过`result.getItem() == Items.AIR`来判断`ItemStack`存放了哪一个`Item`。
21 |
22 | 至于更加详细的解释,[harbinger](https://harbinger.covertdragon.team/chapter-04/item-stack.html)已经写的很清楚了。
23 |
24 |
--------------------------------------------------------------------------------
/src/introducation/intro.md:
--------------------------------------------------------------------------------
1 | # 导论
2 |
3 | 首先欢迎你来到这个教程,既然你会打开这个教程,想必你心中有了开发一个属于自己的mod的念头吧。
4 |
5 | 正好,这个教程也是为这个目的服务的。但是开发一个属于自己的mod并不是一件容易的事情,你需要学习非常多的知识才能达成这个目标,阅读和跟随这个教程只是非常浅显的部分。
6 |
7 | 首先我想让你思考一个问题:**你真的需要自己从头开发一个mod吗?**
8 |
9 | 其实对于大部分人的需求,不需要从零开发一个Mod。有非常多其他的办法可以达成他们的目标:原版内置的机制,MCreator和ZenScript等。
10 |
11 | 如果你的答案是确定的,那么第二个问题:你真的需要亲自写一个mod吗?
12 |
13 | Mod开发需要编程和相当的计算机科学基础,要学好这些并不容易,如果你是一个不会编程的人,或者只是粗略的学过编程,我的建议是寻找同伴。美工和设计在Mod开发中也是不可或缺的一部分。
14 |
15 | 如果你对上面两个问题的答案都是肯定的,那么我觉得你可以开始阅读这个教程了。在这个教程里,我会假设你有一定的计算机科学常识,熟悉Java编程的基础。
16 |
17 | 那么如果你现在还不会编程怎么办?没关系,这里有个教程推荐给你:[Minecraft mod 开发编程入门 ](https://v2mcdev.com/t/topic/88)
18 |
19 | 如果你有任何的问题,请去[论坛](https://v2mcdev.com/)上**按照模版**发帖提问。
20 |
21 | **许可证**
22 |
23 | 
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
24 |
25 |
--------------------------------------------------------------------------------
/src/datapack/lootable.md:
--------------------------------------------------------------------------------
1 | # 掉落物配方
2 |
3 | **请注意,在阅读本章节之前,请先学习数据包的制作与使用,不懂的话请阅读WIKI中相关的内容**
4 |
5 | 同样的,我们先创建目录,这里我们以方块的掉落举例。
6 |
7 | ```
8 | resources
9 | ├── META-INF
10 | │ └── mods.toml
11 | ├── assets
12 | ├── data
13 | │ ├── minecraft
14 | │ └── neutrino
15 | │ ├── loot_tables
16 | │ │ └── blocks
17 | │ └── recipes
18 | │ ├── obsidian_block.json
19 | │ └── obsidian_ingot.json
20 | └── pack.mcmeta
21 | ```
22 |
23 | 在`loot_tables/blocks`下创建和你方块注册名相同的Json文件。这里我们以我们创建的`obsidian_block`为例。
24 |
25 | `obsidian_block.json`:
26 |
27 | ```json
28 | {
29 | "type": "minecraft:block",
30 | "pools": [
31 | {
32 | "rolls": 1,
33 | "entries": [
34 | {
35 | "type": "minecraft:item",
36 | "name": "neutrino:obsidian_block"
37 | }
38 | ],
39 | "conditions": [
40 | {
41 | "condition": "minecraft:survives_explosion"
42 | }
43 | ]
44 | }
45 | ]
46 | }
47 | ```
48 |
49 | 同样的,这里你可以通过`neutrino:obsidian_block`来指定你的物品。
50 |
51 | 可能很多读者,对于原版的`Loot_table`很难理解,这里我可以举个例子,你就把`Loot_table`当作是在赌场里抽奖,里面配置的信息不过就是奖品是什么,可以抽几次,什么条件下可以抽,什么情况下可以多抽几次。
--------------------------------------------------------------------------------
/src/introducation/vanilla.md:
--------------------------------------------------------------------------------
1 | # Minecraft如何运作的
2 |
3 | 这节的内容非常重要,你必须在自己的大脑中构建起Minecraft运行的模型图像,这将会帮助你理解后面涉及到的概念。
4 |
5 | 在这一节中,我将介绍一下Minecraft大体上是怎么运作的,以及一个非常重要的概念:「端」。
6 |
7 | Minecraft大体上属于「C/S架构(客户端/服务端架构)」。那么什么是「服务端」,什么又是「客户端」呢?
8 |
9 | 从名字上其实就能看出大概的意思,「服务端」是用来提供服务的,「客户端」是用户直接使用的。那么这两个端在Minecraft中是怎么体现的呢?
10 |
11 | 在Minecraft中两个端的职责区分如下:
12 |
13 | - 服务端
14 |
15 | 负责游戏的逻辑,数据的读写。
16 |
17 | - 客户端
18 |
19 | 接受用户的输入输出,根据来自服务端的数据来渲染游戏画面。
20 |
21 | 值得注意的是,这里客户端和服务端的区分仅是逻辑上的区分。实际上如果你处于单人模式,那么你的电脑上会同时存在服务端和客户端,而且他们处于不同的线程[^1]。但是当你连接某个服务器时,你的电脑上只存在客户端,服务端被转移到了远程的一台服务器上。
22 |
23 | 下面一张图大概的解释了Minecraft是怎么运作的。
24 |
25 | 
26 |
27 | 看到这张图,你可能觉得奇怪,说好的是服务端负责游戏逻辑的呢,为什么客户端也有数据模型?其实这里的「客户端数据模型」只是「服务端数据模型」一个副本,虽然它们都有独立的游戏Tick,也共享很多相同的代码,但是最终逻辑还是以服务端为准。
28 |
29 | 之前我们提到,客户端和服务端是独立运行的,但是它们不可避免地需要同步数据,而在Minecraft里,所有客户端和服务端的数据同步都是通过网络数据包实现的。在大部分时候原版已经实现好了数据同步的方法,我们只需要调用已经实现好的方法就行,但是在某些情况下,原版没有实现对应的功能,或者不适合使用原版提供的功能,我们就得自己创建和发送网络数据包来完成数据的同步。
30 |
31 | 那么接下去的问题是,我们怎么在代码中区分我们是处于客户端还是服务端呢?
32 |
33 | Minecraft的`World`中有一个`isRemote`字段,当处于客户端时这个变量值为`true`,当处于服务端时这个变量值为`false`。
34 |
35 | ---
36 |
37 | [^1]:线程是程序调度的单位之一,处于不同的线程意味着这两个的逻辑和数据是互相独立的,只能通过特定的方法同步数据。具体来说,服务端处于「Server thread」,客户端处于「Render thread」,如果你有观察过Minecraft启动时的输出日志,应该会看到这两个词。
38 |
39 |
--------------------------------------------------------------------------------
/src/devenvironment/folderintro.md:
--------------------------------------------------------------------------------
1 | # 开发环境的介绍
2 |
3 | 在这一节中,我会介绍一下Mod开发产生的一系列文件和文件夹,以及它们的作用。
4 |
5 | 首先最为重要的一个文件便是:`build.gradle`,这个文件是Gradle的配置文件,它规定了Mod的项目是如何构建,有哪些依赖,如何配置等。
6 |
7 | 其中的`minecraft `闭包下的内容就是关于Forge Gradle的配置。
8 |
9 | 其中`mappings channel: 'snapshot', version: '20190719-1.14.3'`配置项规定了本项目使用的mapping文件版本,这里我强烈建议你经常更新mapping文件,你可以在[这里](http://export.mcpbot.bspk.rs/)找到所有的mapping文件。那么什么是mapping文件呢?还记得我们之间提及的`srg名`和`mcp名`吗?mapping文件的作用就是提供`srg名`和`mcp名`之间的翻译。
10 |
11 | `channel`的意思是mapping文件的分类,在大部分情况下,你都应该使用`snapshot`(快照版本)来确保你的mcp名字是最新的。而之后的`version`就是具体的版本了,大部分情况下是高版本游戏兼容低版本mapping的,当然游戏版本号不能相差太远。其中还有两个被注释起来的参数,这里我们暂且不提。
12 |
13 | 另外一个你可能会用的就是`dependencies`配置,如果你的mod需要依赖别的java库或者别的mod,你需要在这里添加内容,具体添加的方式,注释已经给出了详细的例子,这里就不多说了。其中`minecraft 'net.minecraftforge:forge:1.15.2-31.1.0'`规定了你需要用到的Forge版本,如果你想升级Forge版本可以修改这一行的内容,版本的格式是`net.minecraftforge:forge:游戏版本号-Forge版本号`。
14 |
15 | 这个文件剩余的部分就和一个普通的`build.gradle`没什么差别了,如果想知道更详细的知识建议去学习Gradle。
16 |
17 | 接下去的就是`src`文件夹,这里是放我们代码和资源文件的地方,其中`main`文件夹是具体运行代码和文件的地方,`test`文件夹是放测试代码的地方。`main`文件夹下的`java`就是放我们写的java代码的地方,而`resources`文件夹里放的则是我们的材质模型等一些除了代码之外的属于mod的内容。
18 |
19 | 接下去是`run`文件夹,这个基本上就是一个标准的`.minecraft`文件夹,值得注意的是,因为开发环境是同时有Minecraft客户端和服务器代码的,它们两个是共用`run`目录的。
20 |
21 | 剩下值得一提的就是`build`目录,当你在Gradle面板里运行`build`任务,你的mod就会被打包好放在`build=>libs`下。
22 |
23 | 剩下所有带`gradle`相关的文件夹和文件都是Gradle所需要的运行和配置文件,请不要随便删除。
24 |
25 |
--------------------------------------------------------------------------------
/src/item/group.md:
--------------------------------------------------------------------------------
1 | # 自定义创造模式物品栏
2 |
3 | 在这一节中,我们将研究如何创建一个属于自己的创造模式物品栏,非常简单。
4 |
5 | 首先创建一个类,让它继承`ItemGroup`,`ItemGroup`代表的就是创造模式物品栏,因为我们需要创建一个属于自己的创造模式物品栏,自然需要继承它。
6 |
7 | 内容如下:
8 |
9 | ```java
10 | public class ObsidianGroup extends ItemGroup {
11 | public ObsidianGroup() {
12 | super("obsidian_group");
13 | }
14 |
15 | @Override
16 | public ItemStack createIcon() {
17 | return new ItemStack(ItemRegistry.obsidianIngot.get());
18 | }
19 | }
20 | ```
21 |
22 | 第一个方法用于设置创造模式物品栏的标题名,第二个提供了创造模式物品栏的图标,这里我们用了黑曜石碇作为图标,请注意这个函数的返回值类型是`ItemStack`,而不是`Item`。
23 |
24 | 然后我们需要在实例化这个类,创建`ModGroup`
25 |
26 | ```java
27 | public class ModGroup {
28 | public static ItemGroup itemGroup = new ObsidianGroup();
29 | }
30 | ```
31 |
32 | 在这里我们用来存放`ItemGroup`以及它的子类(比如我们之前创建好`ObsidianGroup`)的实例,这里的每一个实例都代表了游戏中的一个标签栏。
33 |
34 | 创建完成以后想要调用这个物品栏也非常简单,我们以黑曜石碇举例。
35 |
36 | ```java
37 | public class ObsidianIngot extends Item {
38 | public ObsidianIngot() {
39 | super(new Properties().group(ModGroup.itemGroup));
40 | }
41 | }
42 | ```
43 |
44 | 此时打开游戏我们的黑曜石碇应该就在指定的物品栏里了。
45 |
46 | 
47 |
48 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/group)
49 |
50 | ## 编程小课堂
51 |
52 | 如果在编程中遇见了自己不会的事情,第一件事不应该是想着问别人,而是上网搜索,你遇见的绝大部问题别人都已经遇见过了,如果你没有搜索就问别人,是在同时浪费你自己和别人的时间。
--------------------------------------------------------------------------------
/src/datapack/recipes.md:
--------------------------------------------------------------------------------
1 | # 配方
2 |
3 | **请注意,在阅读本章节之前,请先学习数据包的制作与使用,不懂的话请阅读WIKI中相关的内容**
4 |
5 | 在这节中,我们将要来学习如何添加配方,这里我们将以合成配方,和熔炼配方举例。
6 |
7 | 正如原版的配方是放在数据包内,我们Mod的配方也应该放在我们的自己的数据包内,首先第一步就是创建我们Mod的数据包。请按照下面的结构性在你的`resources`文件夹下创建目录。
8 |
9 | ```
10 | resources
11 | ├── META-INF
12 | │ └── mods.toml
13 | ├── assets
14 | ├── data
15 | │ ├── minecraft
16 | │ └── neutrino
17 | │ └── recipes
18 | └── pack.mcmeta
19 | ```
20 |
21 | 其中的`data/neutrino`就是你自己的数据包了。
22 |
23 | 接下在`recipes`文件夹下创建内容。这里你创建的文件名随意,一般情况下我都会写成和物品注册名同名。
24 |
25 | 接下来举两个例子。
26 |
27 | `obsidian_block.json`:
28 |
29 | ```json
30 | {
31 | "type": "minecraft:crafting_shaped",
32 | "pattern": [
33 | "###",
34 | "###",
35 | "###"
36 | ],
37 | "key": {
38 | "#": {
39 | "item": "neutrino:obsidian_ingot"
40 | }
41 | },
42 | "result": {
43 | "item": "neutrino:obsidian_block",
44 | "count": 1
45 | }
46 | }
47 | ```
48 |
49 | `obsidian_ingot.json`
50 |
51 | ```json
52 | {
53 | "type": "minecraft:smelting",
54 | "ingredient": {
55 | "item": "minecraft:obsidian"
56 | },
57 | "result": "neutrino:obsidian_ingot",
58 | "experience": 0.35,
59 | "cookingtime": 200
60 | }
61 | ```
62 |
63 | 可以看到,这里和原版的数据包最大的不同就是我们通过类似`neutrino:obsidian_ingot`指定了,Mod中的物品,而通过`minecraft:obsidian`指定了原版的物品。
64 |
65 | 创建完成以后目录结构如下:
66 |
67 | ```
68 | resources
69 | ├── META-INF
70 | │ └── mods.toml
71 | ├── assets
72 | ├── data
73 | │ ├── minecraft
74 | │ └── neutrino
75 | │ └── recipes
76 | │ ├── obsidian_block.json
77 | │ └── obsidian_ingot.json
78 | └── pack.mcmeta
79 | ```
80 |
81 |
--------------------------------------------------------------------------------
/src/introducation/forge.md:
--------------------------------------------------------------------------------
1 | # Forge是什么
2 |
3 | 本教程是一个基于Forge的Mod开发教程,那么自然而然的要回答一个问题:「Forge是什么?」
4 |
5 | 乍一看,这个好像根本就不是一个问题,「Forge?Forge不就是Forge吗?」看到这个问题的你内心中的第一个浮现出的想法估计就是这个。
6 |
7 | 但是回答这个问题还是非常有必要的,接下去我会稍微讲一讲Forge是什么,以及Forge的历史。这些看上去和我们教程无关的内容,其实是Mod开发领域的「乡谣(Lore)」,学会这些可以更好的让你和其他人交流。
8 |
9 | 我们得从Minecraft本身说起,首先我们得明确Minecraft是一个用Java写成的商业软件。这意味着两件事:第一,Minecraft相对容易修改;第二,代码本身是不开源而且是被混淆过的。在Minecraft历史的早期,因为在Mojang一直都没有给Minecraft提供官方API[^1],所以「Mod Coder Pack」项目诞生了(以下简称为MCP)。
10 |
11 | 还记得我之前说过的,Minecraft的两个特性吗?MCP就利用这两个特性,实现了一套工具,可以让开发者可以直接修改Minecraft jar包里的内容。
12 |
13 | 于是`srg名`,`notch名`和`mcp名`诞生了。
14 |
15 | 那么这三个是什么呢?
16 |
17 | 首先是`notch名`,他是Minecraft直接反编译、反混淆之后的名称,通常是无意义的字母数字组合。你从名称Notch就可以看出,这个名字是直接来自Minecraft(~~以及对Notch的怨念~~),举例来说 `j`就是一个典型的`notch名`。
18 |
19 | 接下来是`srg名`,这个名字是和`notch名`是一一对应的,`srg名`在一个版本里是不会变动的,之所以叫做`srg名`,是为了纪念MCP项目开发的领导者Searge。在`srg名`中,Minecraft中的类名已经是可读了,变量方法等名称虽然还是不可读,但是有相对应的前缀和尾缀来区分了。以上面的`j`为例,它的`srg名`是`func_70114_g`。
20 |
21 | 最后是`mcp名`,这个名称也是我们mod开发中接触最多的名称,在`mcp名`中,代码已经是可读的了。和我们正常写java程序中的名称没什么两样。但是`mcp名`是会变动的。举例来说上面的`func_70114_g`它的`mcp名`是`getCollisionBox`。`mcp名`中的类名和`srg名`中的类名是相同的。
22 |
23 | 接下来我们来讲Forge,随着时间的发展,Mod开发者们意识到,直接修改Jar文件写mod的方式太过于粗暴了,而且Mod和Mod之间的兼容性可以说基本没有,Mod开发者们急需一种工具可以方便地开发Mod,并且能保证mod和mod之间的兼容性,于是Forge就诞生了。
24 |
25 | Forge其实就是一套通过修改Minecraft方式实现的第三方API,而且随着时间的发展,MCP现在已经死亡了,除了Forge这套API,Fabric也风头正盛,而Forge本身也在Minecraft 1.13版本到来之后经历了一次重写,引入了大量函数式编程的API。
26 |
27 | 那么Forge是怎么使用我们之前提及的三个名字的呢?
28 |
29 | 在你安装完Forge之后,游戏的运行过程中,所有的内容都会反编译成`srg名`运行,你编译好的mod同样也会被混淆成`srg名`,保证它可以正常运行。
30 |
31 | ---
32 |
33 | [^1]: API 即 「Application programming interface(应用程序接口)」,是程序的提供的一种机制允许第三方修改或者添加功能。
34 |
35 |
--------------------------------------------------------------------------------
/src/specialmodel/obj.assets/obsidian_obj.obj:
--------------------------------------------------------------------------------
1 | # Blender v2.82 (sub 7) OBJ File: 'tutorial.blend'
2 | # www.blender.org
3 | mtllib obsidian_obj.mtl
4 | o Cube
5 | v 1.000000 0.000000 0.000000
6 | v 1.000000 0.000000 1.000000
7 | v 0.000000 0.000000 0.000000
8 | v 0.000000 0.000000 1.000000
9 | v 0.501437 1.000000 0.498563
10 | v 1.000000 0.501437 0.000000
11 | v 0.501437 1.000000 0.501437
12 | v 1.000000 0.501437 1.000000
13 | v 0.498563 1.000000 0.498563
14 | v 0.000000 0.501437 0.000000
15 | v 0.498563 1.000000 0.501437
16 | v 0.000000 0.501437 1.000000
17 | vt 0.375000 0.750000
18 | vt 0.500359 0.750000
19 | vt 0.500359 1.000000
20 | vt 0.375000 1.000000
21 | vt 0.375000 0.000000
22 | vt 0.500359 0.000000
23 | vt 0.500359 0.250000
24 | vt 0.375000 0.250000
25 | vt 0.125000 0.500000
26 | vt 0.375000 0.500000
27 | vt 0.125000 0.750000
28 | vt 0.749641 0.624641
29 | vt 0.750359 0.624641
30 | vt 0.750359 0.625359
31 | vt 0.749641 0.625359
32 | vt 0.500359 0.500000
33 | vt 0.875000 0.750000
34 | vt 0.625000 0.750000
35 | vt 0.875000 0.500000
36 | vt 0.625000 0.500000
37 | vn 0.0000 0.0000 1.0000
38 | vn -1.0000 0.0000 0.0000
39 | vn 0.0000 -1.0000 0.0000
40 | vn 0.0000 1.0000 0.0000
41 | vn 1.0000 0.0000 0.0000
42 | vn 0.0000 0.7071 0.7071
43 | vn 0.7071 0.7071 0.0000
44 | vn -0.7071 0.7071 0.0000
45 | vn 0.0000 0.7071 -0.7071
46 | vn 0.0000 0.0000 -1.0000
47 | usemtl Material
48 | s off
49 | f 2/1/1 8/2/1 12/3/1 4/4/1
50 | f 4/5/2 12/6/2 10/7/2 3/8/2
51 | f 3/9/3 1/10/3 2/1/3 4/11/3
52 | f 5/12/4 9/13/4 11/14/4 7/15/4
53 | f 1/10/5 6/16/5 8/2/5 2/1/5
54 | f 7/15/6 11/14/6 12/17/6 8/18/6
55 | f 5/12/7 7/15/7 8/2/7 6/16/7
56 | f 11/14/8 9/13/8 10/19/8 12/17/8
57 | f 9/13/9 5/12/9 6/20/9 10/19/9
58 | f 3/8/10 10/7/10 6/16/10 1/10/10
59 |
--------------------------------------------------------------------------------
/src/io/intro.md:
--------------------------------------------------------------------------------
1 | # 用户输入
2 |
3 | 这一节中我们将来学习如何获取用户输入。这里我们将以快捷键举例。请注意,所有的用户输入都是客户端行为。
4 |
5 | ```java
6 | @Mod.EventBusSubscriber(value = Dist.CLIENT)
7 | public class KeyBoardInput {
8 | public static final KeyBinding MESSAGE_KEY = new KeyBinding("key.message",
9 | KeyConflictContext.IN_GAME,
10 | KeyModifier.CONTROL,
11 | InputMappings.Type.KEYSYM,
12 | GLFW.GLFW_KEY_J,
13 | "key.category.neutrino");
14 |
15 | @SubscribeEvent
16 | public static void onKeyboardInput(InputEvent.KeyInputEvent event) {
17 | if (MESSAGE_KEY.isPressed()) {
18 | assert Minecraft.getInstance().player != null;
19 | Minecraft.getInstance().player.sendMessage(new StringTextComponent("You Press J"));
20 | }
21 | }
22 | }
23 |
24 | ```
25 |
26 | 首先我们创建了一个`KeyBinding`,这个就是一个可以配置的快捷键,具体的参数非常简单,这里就不多加说明了,别忘了`value = Dist.CLIENT`,因为用户输入是物理客户端才有的东西。
27 |
28 | 可以看到,我们监听了Forge总线上的`InputEvent.KeyInputEvent`,即键盘输入事件。`InputEvent`下还有其他子事件,大家可以按需选用。在这里我们判断当我的快捷键按下时,给玩家发送一个消息。
29 |
30 | 当然我们还需要注册我们的快捷键。
31 |
32 | ```java
33 | @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
34 | public class KeybindingRegistry {
35 | @SubscribeEvent
36 | public static void onClientSetup(FMLClientSetupEvent event) {
37 | ClientRegistry.registerKeyBinding(KeyBoardInput.MESSAGE_KEY);
38 | }
39 | }
40 | ```
41 |
42 | 因为按钮按下是个客户端行为,所有这里我们选用`FMLClientSetupEvent`事件,在里面调用`ClientRegistry.registerKeyBinding`方法注册我们的快捷键,同样的在这里别忘了`value = Dist.CLIENT`。
43 |
44 | 打开游戏按下我们之前设置的快捷键,就可以看到我们的消息了。
45 |
46 | 
47 |
48 | 
49 |
50 | 注:因为我的系统时macOS,所以这里显示的是CMD。
51 |
52 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/input)
53 |
54 |
--------------------------------------------------------------------------------
/src/item/modelandtextures.md:
--------------------------------------------------------------------------------
1 | # 物品材质与模型
2 |
3 | 在上一节中我们已经成功添加了第一个物品,当然那个物品还很丑,在这一节中我们将会为它添加模型和材质。
4 |
5 | 首先按照如下目录在`resources`下创建文件夹。
6 |
7 | ```
8 | resources
9 | ├── META-INF
10 | │ └── mods.toml
11 | ├── assets
12 | │ └── neutrino
13 | │ ├── models
14 | │ │ └── item
15 | │ └── textures
16 | │ └── item
17 | └── pack.mcmeta
18 | ```
19 |
20 | 其实`assets`下就是一个属于Mod的材质包,具体的目录结构等,读者可以自行寻找当前游戏版本的材质包制作教程学习。
21 |
22 | 接下来我们来添加模型文件,首先在在`models`下的`item`里,创建一个和你添加的物品,有着相同注册名的json文件,在我们的例子里就是`obsidian_ingot.json`。
23 |
24 | 内容如下:
25 |
26 | ```json
27 | {
28 | "parent": "item/generated",
29 | "textures": {
30 | "layer0": "neutrino:item/obsidian_ingot"
31 | }
32 | }
33 | ```
34 |
35 | 这里的内容非常简单:`"parent": "item/generated”`指定了这个模型的「父模型」是什么,而`"layer0": "neutrino:item/obsidian_ingot”`指定了具体的材质。`neutrino:`代表这个是在我们自己的`assets`文件下,`item/obsidian_ingot`代表了是`textures/item/obsidian_ingot.png`这张图片。
36 |
37 | 模型文件的详细格式大家可以自行阅读[Wiki]([https://minecraft-zh.gamepedia.com/index.php?title=%E6%A8%A1%E5%9E%8B&variant=zh](https://minecraft-zh.gamepedia.com/index.php?title=模型&variant=zh))。
38 |
39 | 接下来我们在`textures/item/obsidian_ingot.png`下放入我们制作好的材质文件,请注意材质文件的比例是1:1,并且最好不要大于32x32像素。
40 |
41 |
42 |
43 | **这里的加载流程是:游戏先根据的你注册名获取相对应的模型文件,然后通过模型文件中的`textures`加载对应的材质文件。**
44 |
45 | 创建完成的目录树如下:
46 |
47 | ```
48 | resources
49 | ├── META-INF
50 | │ └── mods.toml
51 | ├── assets
52 | │ └── neutrino
53 | │ ├── models
54 | │ │ └── item
55 | │ │ └── obsidian_ingot.json
56 | │ └── textures
57 | │ └── item
58 | │ └── obsidian_ingot.png
59 | └── pack.mcmeta
60 |
61 | ```
62 |
63 | 启动游戏之后你就可以看见我们有了模型和材质的物品了。
64 |
65 | 
66 |
67 | ## 开发小课堂
68 |
69 | 一个方便的工具用来制作方块和物品等模型:[BlockBench](https://blockbench.net/)。
--------------------------------------------------------------------------------
/src/event/intro.md:
--------------------------------------------------------------------------------
1 | # 事件系统
2 |
3 | 在开始我们接下来的教程之前,我们得先讲讲事件系统。在之前的章节中,我一直用`DeferredRegister`系统掩盖了直接的事件系统,可惜的是这已经无法再隐藏下去了,在这一节中,我们将学习事件系统,为了读者方便理解,如果你已经忘了总线,事件和事件处理器是什么的话,请先翻阅之前的内容。
4 |
5 | 首先要说明的是这个事件系统并不是Minecraft自带的,这个事件系统是Forge实现的。
6 |
7 | 你可以用两种方式来使用事件系统,一种是实例方式,一种是静态的方式。
8 |
9 | ```java
10 | public class MyForgeEventHandler {
11 | @SubscribeEvent
12 | public void pickupItem(EntityItemPickupEvent event) {
13 | System.out.println("Item picked up!");
14 | }
15 | }
16 | ```
17 |
18 | 我们先从实例的方法开始说起。可以看到这里最为特殊的就是`@SubscribeEvent`注解,这个注解的作用就是标记下方的`pickupItem` 方法是一个事件处理器,至于它具体监听的事件是由它的参数类型决定的,在这里它的参数类型是`EntityItemPickupEvent`,说明它监听的是实体捡起物品这个事件。
19 |
20 | 当然,对于实例方式的事件处理这样还不够,我们还得手动在某个地方实例化它并把它注入到事件总线里,我们之前说过Minecraft里有两条事件总线「Forge总线」和「Mod总线」,Mod总线主要负责游戏的生命周期事件,也就是初始化过程的事件,而Forge总线负责的就是除了生命周期事件外的所有事件。你可以用`MinecraftForge.EVENT_BUS.register()`方法将你的事件实例注册到Forge总线中,也可用`FMLJavaModLoadingContext.get().getModEventBus().register()`方法将其注册到Mod总线中,一般情况下你应该在你的Mod主类的初始化方法里注册这些事件。
21 |
22 | 在我们的例子里就是如下:
23 |
24 | ```java
25 | MinecraftForge.EVENT_BUS.register(new MyForgeEventHandler());
26 | ```
27 |
28 | 当然所有的事件处理器都要手动注册非常的麻烦,Forge同样提供了一个静态的注册事件的方法。内容如下:
29 |
30 | ```java
31 | @Mod.EventBusSubscriber()
32 | public class MyStaticClientOnlyEventHandler {
33 | @SubscribeEvent
34 | public static void drawLast(RenderWorldLastEvent event) {
35 | System.out.println("Drawing!");
36 | }
37 | }
38 | ```
39 |
40 | 可以看到相比之前的代码这里有两点不同,首先多了一个`@Mod.EventBusSubscriber()`注解,这个注解就是用来标注这个类里面的所有打了` @SubscribeEvent`静态方法都是事件处理器,这个注解里有好几个参数,其中最为常用的就是用来指定你所用注入的总线是什么,在默认情况下下面的事件处理注入是Forge总线,你可以用`@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)`来指定你要注入到Mod总线中。当然这里的参数不止一个,大家可以自行查看`@Mod.EventBusSubscriber`的具体内容,关于可以使用的变量都要详细的注释。
41 |
42 | 这里第二点不同就是我们的事件处理函数也变成了静态的,这是在你还不熟悉事件系统使用时非常容易出错的地方,请务必注意。
43 |
44 | 作为我个人的偏好,我更喜欢后一种静态的事件处理方式,之后的教程也会使用这个方式。
45 |
46 | 当然事件系统还有很多功能,比如取消,设置结果以及设置优先级,限于篇幅大家可以自行阅读Forge的[文档](https://mcforge.readthedocs.io/en/latest/events/intro/)
47 |
48 |
--------------------------------------------------------------------------------
/src/item/food.md:
--------------------------------------------------------------------------------
1 | # 食物
2 |
3 | 在这一节中我们将会在Minecraft世界中添加一个新的食物:黑曜石苹果,吃了这个苹果以后你可以回复饥饿,但是会中毒。和很多人想象的不一样,食物并不是单独的一个东西,对于Minecraft来说,食物只是一种特殊的物品而已。
4 |
5 | 同样的我们先来创建一个类,让这个类继承`Item`
6 |
7 | ```java
8 | public class ObsidianApple extends Item {
9 | private static EffectInstance effectInstance = new EffectInstance(Effects.POISON, 3 * 20, 1);
10 | private static Food food = (new Food.Builder())
11 | .saturation(10)
12 | .hunger(20)
13 | .effect(effectInstance, 1)
14 | .build();
15 |
16 | public ObsidianApple() {
17 | super(new Properties().food(food).group(ItemGroup.FOOD));
18 | }
19 | }
20 |
21 | ```
22 |
23 | 我们一行一行的解释。
24 |
25 | 首先我们创建了一个`EffectInstance`,什么是`EffectInstance`?`EffectInstance`正如他名字暗示的那样是一个药水效果的实例。我们先来思考一下,原版的在玩家身上的药水效果都有哪些属性:效果的种类、时间以及药水等级。而`EffectInstance`就是这三种属性的一个集合。从`new EffectInstance(Effects.POISON, 3 * 20, 1)`之中可以看到我们填入的三种属性分别是:原版的中毒药水效果(原版所有的药水效果都在`Effects`类内)、持续时间是`3*20`tick,药水等级为`1`。
26 |
27 | 接下来我们创建了一个`Food`类型的变量,这个变量规定了这个这个食物的一些属性,比如:`saturation`方法设置了饱食度,`hunger`设置了回复的饥饿度,`effect`方法设置了吃食物时可能会有的药水效果,其中第二个参数代表触发效果的可能性(想想原版的生鸡肉),这里我们设置成`1`代表100%触发。这里其实用到称为「建造者模式」的设计模式,有兴趣的同学可以自己查阅。
28 |
29 | 接下去就是构造方法,想必大家已经很熟悉了,唯一新的一点就是`.food(food)`,这个方法表明了物品是一个食物,最后我们把这个物品放在了「食物」创造模式物品栏里。
30 |
31 | 然后我们注册我们的食物,注册名是`obsidian_apple`:
32 |
33 | ```java
34 | public static RegistryObject- obsidianApple = ITEMS.register("obsidian_apple", () -> {
35 | return new ObsidianApple();
36 | });
37 | ```
38 |
39 | 然后添加模型`obsdian_ingot.json`:
40 |
41 | ```json
42 | {
43 | "parent": "item/generated",
44 | "textures": {
45 | "layer0": "neutrino:item/obsidian_apple"
46 | }
47 | }
48 | ```
49 |
50 | 然后是材质`obsidian_apple.png`
51 |
52 |
53 |
54 | 拿出你的苹果试着吃吃看吧。
55 |
56 | 
57 |
58 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/food)
59 |
60 |
--------------------------------------------------------------------------------
/src/worldgeneration/intro.md:
--------------------------------------------------------------------------------
1 | # 世界生成
2 |
3 | 在这节中,我们将要来学习Minecraft中非常迷人的一部分:「世界生成」。Minecraft的世界生成可以说是Minecraft 能如此的受欢迎的一个基础所在。而我们接下来就要学习如何一步一步的自定义我们的世界生成。
4 |
5 | 在1.13之后Minecraft重写了世界生成相关的代码,世界生成和修改变得容易的很多,在开始之前我们得要明确一些类和它相对应的作用。
6 |
7 | 首先一个类是`World`,其实和世界生成直接相关的是它的子类`ServerWorld`(因为世界生成的计算都是服务端进行的)。这个是代表的你的存档游戏运行环境,在`World`类下面有个`WorldInfo worldInfo`的变量,里面存放了类似于当前的存档`WorldType`(超平坦、巨大化、普通)等信息。
8 |
9 | 
10 |
11 | 下属于`World`类的是`Dimension`(维度),在一个世界里可以有很多个维度,比如在我们正常游戏时,就会接触到三个`Dimension`:`EndDimension`、`NetherDimension`和`OverworldDimension`,也就是末地、下届和主世界三个维度。
12 |
13 | 
14 |
15 | 在`Dimension`之下就是是`ChunkGenerator`,正如它的名字所暗示的那样,这个类和其子类的作用就是在Minecraft中产生一个接一个的`Chunk`区块,而Minecraft这连绵不断的地形生成也正是它产生的。在正常游戏时末地、下届和主世界的区块生成分别对应着`EndChunkGenerator`、`NetherChunkGenerator`和`OverworldChunkGenerator`。如果你查看它们的继承关系的话就会发现它们都继承了`NoiseChunkGenerator`这个类就是Minecraft利用柏林噪音实现了地形生成算法的地方。
16 |
17 | 
18 |
19 | 接下来在`ChunkGenerator`之下的就是就是`Biome` (生态群系)。你在游戏中能见到的所有生态群系都是这个类的子类,这个类规定了生态群系的种种特征。之所以`Biome`是在`ChunkGenerator`之下,是因为一个`Chunk`中可以同时存在好几个生态群系(当`Chunk`在生态群系的边界时)。
20 |
21 | 
22 |
23 | 
24 |
25 | 可以看到在同一个区块里可以同时存在多个生物群系。
26 |
27 | 以下是继承图的节选。
28 |
29 | 
30 |
31 | 最后在`Biome`之下的是生物群系的四种属性`Feature`、`Carver`、`Structure`以及`Spawn`。他们的代表的东西如下图:
32 |
33 | | 特性 | 作用 |
34 | | ----------- | -------------------------------------------------------- |
35 | | `Feature` | 地下的岩浆湖、矿物、废弃矿洞、地面上的花等 |
36 | | `Carver` | 地下的洞穴等 |
37 | | `Structure` | 村庄、林地府邸等(注:`Structure`其实是`Feature`的子类) |
38 | | `Spawn` | 在这个生物群系下会生成的生物 |
39 |
40 | ---
41 |
42 | 在你阅读世界生成相关的代码时可能会看见类似于`NetherGenSettings`名字末尾带个`Settings`的类,不用太害怕这种类,他们的作用只是为了配置一些属性。一个种是类似于`OverworldBiomeProvider`这样名字后面带一个`Provider`的类,这其实是一种叫做`Provider model(提供者模型)`的设计模式,当你看到这种类,只需要知道这些类最后会产生出他们去掉名字中`Provider`部分的类。
--------------------------------------------------------------------------------
/src/i18n/i18n.md:
--------------------------------------------------------------------------------
1 | # 语言文件与本地化
2 |
3 | 在这一节中,我们将学习如何给我的物品添加名称。
4 |
5 | 在前面的章节,我们创建的物品的名字看上去是一串没有意义的字符,在这一节中我们将给他们加上一个有意义的名字。
6 |
7 | 在Minecraft中,文件的名字是以语言文件的形式提供的,在1.15的版本,语言文件是个json文件,它大概内容如下:
8 |
9 | ```json
10 | {
11 | "language.name": "English",
12 | "language.region": "United States",
13 | "language.code": "en_us",
14 | "narrator.button.accessibility": "Accessibility",
15 | "narrator.button.language": "Language"
16 | }
17 | ```
18 |
19 | 可以看见一个语言文件其实就是一个「键值对」,其中的「键」是一个游戏中的编号,其中的「值」就是具体的翻译。这么做的原因是Minecraft要支持非常多国家和地区的语言,各个语言如果直接硬编码进游戏里显然是不具备可维护性的。在默认情况下,如果你没有给在游戏中给需要翻译的对象添加相对应的翻译,那么它默认显示的就是这个翻译的「键」。
20 |
21 | 以我们的黑曜石碇举例:
22 |
23 | 
24 |
25 | 目前我们还没有为它添加名字,它所显示的`item.neutrino.obsidian_ingot`就是默认的键值。当然大家也可以自定义键值,Item类里有相对应的方法可以实现这点,正如我之前所说的,在Minecraft源代码里和通过函数名猜测功能是Mod开发必备的能力,所以这里相当于一个小测试,请大家自己寻找可以修改这个键的办法。
26 |
27 | 但是在有些时候,游戏无法默认地给我的内容自动添加键,这时我们就得自己创建一个键,Minecraft提供了一个叫做`I18n.format`的方法让我自己创建,具体的使用方式,我们之后会讲到。
28 |
29 | 接下来让我创建语言文件吧。
30 |
31 | 首先在`neutrino`文件夹下创建一个叫做`lang`的文件夹,创建成功后目录树如下:
32 |
33 | ```
34 | resources
35 | ├── META-INF
36 | │ └── mods.toml
37 | ├── assets
38 | │ └── neutrino
39 | │ ├── lang
40 | │ ├── models
41 | │ │ └── item
42 | │ │ ├── obsidian_apple.json
43 | │ │ ├── obsidian_ingot.json
44 | │ │ └── obsidian_sword.json
45 | │ └── textures
46 | │ └── item
47 | │ ├── obsidian_apple.png
48 | │ ├── obsidian_ingot.png
49 | │ └── obsidian_sword.png
50 | └── pack.mcmeta
51 | ```
52 |
53 | 这里我们以简体中文举例举例。
54 |
55 | 首先创建一个叫做`zh_cn.json`的文件,内容如下。
56 |
57 | ```json
58 | {
59 | "item.neutrino.obsidian_ingot": "黑曜石锭",
60 | "item.neutrino.obsidian_apple":"黑曜石苹果",
61 | "item.neutrino.obsidian_sword":"黑曜石剑",
62 | "itemGroup.obsidian_group": "黑曜石物品栏"
63 | }
64 | ```
65 |
66 | 然后启动游戏,调成简体中文,你应该就可以看见我们的物品有了翻译。
67 |
68 | 作为一个简体中文的使用者,你的mod里至少应该有`zh_cn.json`,`en_us.json`和`zh_tw.json`这三个语言文件,所有可用的语言文件列表请在Wiki的[语言#可用语言](https://minecraft-zh.gamepedia.com/index.php?title=语言&variant=zh#可用语言)内查看。
--------------------------------------------------------------------------------
/src/block/modelandtextures.md:
--------------------------------------------------------------------------------
1 | # 方块模型和材质
2 |
3 | 我们已经成功地创建了一个方块,但是这个方块还很丑,就是一个紫黑块而已。在这一节中,我们就要来为这个方块添加模型和材质。
4 |
5 | 首先我们创建一些文件夹,创建成功后的目录如下显示:
6 |
7 | ```
8 | resources
9 | ├── META-INF
10 | │ └── mods.toml
11 | ├── assets
12 | │ └── neutrino
13 | │ ├── blockstates
14 | │ ├── lang
15 | │ ├── models
16 | │ │ ├── block
17 | │ │ └── item
18 | │ └── textures
19 | │ ├── block
20 | │ └── item
21 | └── pack.mcmeta
22 | ```
23 |
24 | 然后我们在`blockstates`文件夹下,创建一个和你物品注册名同名的json文件,这里我们要创建`obsidian_block.json`
25 |
26 | 内容如下:
27 |
28 | ```json
29 | {
30 | "variants": {
31 | "": { "model": "neutrino:block/obsidian_block_model" }
32 | }
33 | }
34 | ```
35 |
36 | 这个文件其实就是方块状态和具体要使用的模型的映射表,如果你还不清楚什么是方块状态,请向前翻阅。
37 |
38 | 这里我们没有方块状态,所以写了`"": { "model": "neutrino:block/obsidian_block_model” }`,将默认模型设置成了`obsidian_block_model.json`。
39 |
40 | 接下来我们在`models/block`下创建`obsidian_block_model.json`,内容如下:
41 |
42 | ```json
43 | {
44 | "parent": "block/cube_all",
45 | "textures": {
46 | "all": "neutrino:block/obsidian_block_texture"
47 | }
48 | }
49 | ```
50 |
51 | 可以看到,和物品模型相比,只是继承的东西不太一样,至于具体模型文件的格式请参考[Wiki](https://minecraft-zh.gamepedia.com/index.php?title=模型&variant=zh)。在模型里,我们调用了`obsidian_block_texture.png`作为我们的材质。
52 |
53 | 接下来让我们在`textures/block`下添加我们的材质,同样地请注意材质文件的比例是1:1,并且最好不要大于32x32像素。
54 |
55 |
56 |
57 |
58 |
59 | 这时启动游戏,你应该就可看见我们的方块有对应的材质和模型了。
60 |
61 | 
62 |
63 | **整个加载过程为:获取游戏中方块的状态,在Blockstates相对应的映射表里获取模型,根据模型加载材质。**
64 |
65 | 但这时你会发现我们方块相对应的物品还没有材质,接下来我们就要解决这个问题。
66 |
67 | 在`models/item`下创建和我们BlockItem注册名同名的json文件,这里是`obsidian_block.json`
68 |
69 | ```json
70 | {
71 | "parent": "neutrino:block/obsidian_block_model"
72 | }
73 | ```
74 |
75 | 对,你没看错,就一句话,我们直接继承相对应的方块模型就行了。
76 |
77 | 
78 |
79 | ## 开发小课堂
80 |
81 | 在调试模型材质或者是某个函数的过程中,你可能需要多次重启,其实是有办法规避这个问题的。首先你需要在「Debug(调试模式」下启动游戏,然后选择上方的「build->Build(构建项目=>构建项目)」,只要是函数内部的修改,都可以热更新。对于模型和材质还得多一个步骤,在构建项目结束后按F3+T重新载入材质包。当然这个方法也不是万能的,当你发现没法热更新或者热更新不起效时,你还是得重启游戏。
--------------------------------------------------------------------------------
/src/sounds/intro.md:
--------------------------------------------------------------------------------
1 | # 音效
2 |
3 | 在这节中,我们讲学习如何向给我的Mod添加音效,非常的简单,就让我开始吧。
4 |
5 | ```java
6 | public class SoundEventRegistry {
7 | public static final DeferredRegister SOUNDS = new DeferredRegister<>(ForgeRegistries.SOUND_EVENTS, "neutrino");
8 | public static RegistryObject meaSound = SOUNDS.register("mea", () -> {
9 | return new SoundEvent(new ResourceLocation("neutrino", "mea"));
10 | });
11 | }
12 | ```
13 |
14 | 就这两句话,就可以注册我们的音效了。
15 |
16 | 接下来我们来添加音效,在你的`Resource`目录下创建如下的文件和目录。
17 |
18 | ```
19 | resources
20 | ├── META-INF
21 | │ └── mods.toml
22 | ├── assets
23 | │ └── neutrino
24 | │ ├── blockstates
25 | │ ├── lang
26 | │ ├── models
27 | │ ├── sounds
28 | │ ├── sounds.json
29 | │ └── textures
30 | ├── data
31 | └── pack.mcmeta
32 | ```
33 |
34 | 在这里我们创建了一个`sounds.json`,这个文件的具体格式请参照Wiki。
35 |
36 | 内容如下
37 |
38 | ```json
39 | {
40 | "mea": {
41 | "subtitle": "mea",
42 | "replace": "true",
43 | "sounds": [
44 | {
45 | "name": "neutrino:mea",
46 | "steam": true
47 | }
48 | ]
49 | }
50 | }
51 | ```
52 |
53 | 请注意,最外面的键名和你之前`ResourceLocation`中第二个参数应该是一样的。然后我们在`name`中指定了具体的声音文件。请注意,Minecraft只允许载入ogg格式的音频文件。
54 |
55 | 接下来,我们将制作好的音频文件放入`sounds`文件内。
56 |
57 | ```
58 | resources
59 | ├── META-INF
60 | │ └── mods.toml
61 | ├── assets
62 | │ └── neutrino
63 | │ ├── blockstates
64 | │ ├── lang
65 | │ ├── models
66 | │ ├── sounds
67 | │ │ └── mea.ogg
68 | │ ├── sounds.json
69 | │ └── textures
70 | ├── data
71 | └── pack.mcmeta
72 | ```
73 |
74 | 放置好的目录如上。
75 |
76 | 接下来就可创建物品来使用我们的音效了
77 |
78 | ```java
79 | public class SoundTestItem extends Item {
80 | public SoundTestItem() {
81 | super(new Properties().group(ModGroup.itemGroup));
82 | }
83 |
84 | @Override
85 | public ActionResult onItemRightClick(World worldIn, PlayerEntity playerIn, Hand handIn) {
86 | if (worldIn.isRemote) {
87 | worldIn.playSound(playerIn, playerIn.getPosition(), SoundEventRegistry.meaSound.get(), SoundCategory.AMBIENT, 10f, 1f);
88 | }
89 | return super.onItemRightClick(worldIn, playerIn, handIn);
90 | }
91 | }
92 | ```
93 |
94 | 打开游戏试试吧,我们应该成功添加了音效。
95 |
96 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/sounds)
97 |
98 |
--------------------------------------------------------------------------------
/src/worldgeneration/oregeneration.md:
--------------------------------------------------------------------------------
1 | # 矿物生成
2 |
3 | 在这节中我们将来学习如何进行矿物生成。在学习这件之前请阅读本章的介绍,对世界生成有个大概的印象。
4 |
5 | 正如我们之前所说的,矿物生成属于`Biome` 的个`Feature`,而如果你想要添加矿物生成,其实也就是向生物群系中添加一个新的`Feature`。接下来看代码
6 |
7 | ```java
8 | @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
9 | public class OreGen {
10 | @SubscribeEvent
11 | public static void onSetUpEvent(FMLCommonSetupEvent event) {
12 | for (Biome biome : ForgeRegistries.BIOMES) {
13 | biome.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES,
14 | Feature.ORE.withConfiguration(
15 | new OreFeatureConfig(OreFeatureConfig.FillerBlockType.NATURAL_STONE,
16 | BlockRegistry.obsidianBlock.get().getDefaultState(),
17 | 3)
18 | ).withPlacement(Placement.COUNT_DEPTH_AVERAGE.configure(new DepthAverageConfig(30, 30, 20)))
19 | );
20 | }
21 | }
22 | }
23 | ```
24 |
25 | 没错就这么简单。
26 |
27 | 因为所有的世界生成都是服务端的行为,所以这里我们自然需要订阅`FMLCommonSetupEvent`事件。
28 |
29 | 然后我们通过`ForgeRegistries.BIOMES`获取所有注册好的生物群系,并且调用`biome.addFeature`向生物群系中添加`Feature`(也就是我们的矿物)。`Feature`有不同的阶段,这里我们填入的是`GenerationStage.Decoration.UNDERGROUND_ORES`代表矿物生成阶段。然后就是通过`Feature.ORE.withConfiguration`来创建一个矿物生成的`Feature`。
30 |
31 | ```java
32 | Feature.ORE.withConfiguration(
33 | new OreFeatureConfig(OreFeatureConfig.FillerBlockType.NATURAL_STONE,
34 | BlockRegistry.obsidianBlock.get().getDefaultState(),
35 | 3))
36 | .withPlacement(Placement.COUNT_DEPTH_AVERAGE.configure(new DepthAverageConfig(30, 30, 20)))
37 | )
38 | ```
39 |
40 | `Feature.ORE.withConfiguration`需要传入一个`OreFeatureConfig`用来配置我们的矿物生成`Feature`,因为我们是在主世界生成矿物,所以一个参数填入的是`OreFeatureConfig.FillerBlockType.NATURAL_STONE`,第二个参数指定你要生成方块的默认状态,这里我们选择生成我们之前创建的黑曜石方块,第三个参数控制了每次生成的最大数量。
41 |
42 | 光这样你的矿物还没发生成,因为你还没有指定你的矿物需要生成在哪里,生成几次。这就是`withPlacement`的作用,这里你要传入一个`Placement`,这个`Placement`是用来控制你的`Feature`需要生成在哪里的,原版提供了很多的`Placement`,这里我们使用`Placement.COUNT_DEPTH_AVERAGE`,调用它的`configure`的方法,里面需要传入一个`DepthAverageConfig`,这里的三个参数分别控制的,每个区块的生成次数,最低生成高度,以及生成范围。
43 |
44 | 至此,我们的矿物生成以及创建完毕。
45 |
46 | 
47 |
48 | 可以看见我们的矿物正常的生成了。
49 |
50 | 如果你想要更加复杂的自定义矿物生成,可以重写`Feature`。
51 |
52 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/oregen)
53 |
54 |
--------------------------------------------------------------------------------
/src/capability/simpleusage.md:
--------------------------------------------------------------------------------
1 | # 开始使用预定义能力
2 |
3 | 在这一节中我们来学习如何使用Forge已经提供好的Capability。
4 |
5 | Forge为我们已经预定义好了一下几种Capability,你可通过查看`@CapabilityInject`注释在原版内容中的使用来找到预定于好的Capability。
6 |
7 | - `ANIMATION_CAPABILITY`,接口`IAnimationStateMachine`,这个能力与动画有关
8 | - `ENERGY`,接口`IEnergyStorage`,这个能力就是大家可能都听说过的Forge Energy
9 | - `FLUID_HANDLER_CAPABILITY` ,接口`IFluidHandler` ,这个能力和方块形式的流体有关
10 | - `FLUID_HANDLER_ITEM_CAPABILITY`,接口`IFluidHandlerItem`,这个能力和物品形式的流体有关
11 | - `ITEM_HANDLER_CAPABILITY`,接口`IItemHandler`,这个能力和物品的输入输出有关
12 |
13 | 在这节中,我们将会以`ITEM_HANDLER_CAPABILITY`,做一个简单的演示。如果想要更详细的例子,可以看看ustc-zzzz中关于Forge 能力系统介绍的[博客](https://blog.ustc-zzzz.net/forge-energy-demo-1/)。
14 |
15 | 在这里我们要实现一个只能从上方倒入并且可以帮我们自动删除圆石的垃圾桶。
16 |
17 | ```java
18 | public class ObsidianTrashTileEntity extends TileEntity {
19 | public ObsidianTrashTileEntity() {
20 | super(TileEntityTypeRegistry.obsidianTrash.get());
21 | }
22 |
23 | @Nonnull
24 | @Override
25 | public LazyOptional getCapability(@Nonnull Capability cap, @Nullable Direction side) {
26 | if (side == Direction.UP && cap == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY) {
27 | return LazyOptional.of(() -> {
28 | return new ItemStackHandler() {
29 | @Override
30 | public boolean isItemValid(int slot, @Nonnull ItemStack stack) {
31 | return stack.getItem() == Items.COBBLESTONE;
32 | }
33 | };
34 | }).cast();
35 | }
36 | return super.getCapability(cap, side);
37 | }
38 | }
39 | ```
40 |
41 | ```java
42 | if (side == Direction.UP && cap == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY)
43 | ```
44 |
45 | 在这里我们规定了我们的垃圾桶只有从上方导入时才有效,并且声明了有`ITEM_HANDLER_CAPABILITY`这个能力。
46 |
47 | ```java
48 | return LazyOptional.of(() -> {
49 | return new ItemStackHandler() {
50 | @Override
51 | public boolean isItemValid(int slot, @Nonnull ItemStack stack) {
52 | return stack.getItem() == Items.COBBLESTONE;
53 | }
54 | };
55 | }).cast();
56 | ```
57 |
58 | 然后在这里创建了一个`ItemStackHandler`实例,这个是Forge内置的已经实现了`IItemHandler`接口的类,大家可以通过查看`IItemHandler`的基础树来找到这个类。
59 |
60 | 并且我们在创建这个实例的时候复写了`isItemValid`方法用来过滤物品,至于这些方法都有什么作用,请大家自行阅读`IItemHandler`接口的注释。
61 |
62 | 因为这里的能力是内置了,所以我们就不需要注册了。
63 |
64 | 至于方块,物品等注册,请读者自行完成。
65 |
66 | 打开游戏试试吧。
67 |
68 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/use_cap)
--------------------------------------------------------------------------------
/src/configure/configure.md:
--------------------------------------------------------------------------------
1 | # 配置文件
2 |
3 | 在这节中,我们将要来学习配置文件的编写。在1.12之后,Forge将配置文件格式改成Toml。
4 |
5 | 现在我们开始吧
6 |
7 | 为了理解方便,我先把最后生成出的配置文件格式粘贴出来
8 |
9 | ```toml
10 | #General settings
11 | [general]
12 | #Test config value
13 | #Range: > 0
14 | value = 10
15 | ```
16 |
17 | ```java
18 | public class Config {
19 | public static ForgeConfigSpec COMMON_CONFIG;
20 | public static ForgeConfigSpec.IntValue VALUE;
21 |
22 | static {
23 | ForgeConfigSpec.Builder COMMON_BUILDER = new ForgeConfigSpec.Builder();
24 | COMMON_BUILDER.comment("General settings").push("general");
25 | VALUE = COMMON_BUILDER.comment("Test config value").defineInRange("value", 10, 0, Integer.MAX_VALUE);
26 | COMMON_BUILDER.pop();
27 | COMMON_CONFIG = COMMON_BUILDER.build();
28 | }
29 | }
30 | ```
31 |
32 | 创建一个配置文件可以大致分为一下几个部分,首先你得创建一个`Builder`,然后向这个`Builder`塞入你需要的配置选项,以及注释等,最后调用其`build`方法,构建出我们的配置实例。
33 |
34 | 我们先来看静态代码块中的内容。
35 |
36 | 首先我们调用`ForgeConfigSpec.Builder()`创建了一个`Builder`。
37 |
38 | 接下来的`push`和`pop`是一组方法,必须配合使用,每一个`comment`对应了配置文件中一个节(也就是中括号的部分),其中的`push`规定了节的名字,`comment`则是添加了注释。
39 |
40 | `ForgeConfigSpec.IntValue`规定了我们的值,这里的值的允许的种类可以有:EnumValue、LongValue、IntValue、BooleanValue、DoubleValue。
41 |
42 | 我们在这里通过`defineInRange`方法定义了我们配置文件中选项的名字,默认值,以及值的范围。
43 |
44 | 最后我们通过`COMMON_BUILDER.build()`,构建出了我们配置文件的实例。
45 |
46 | 当然,你还需要注册这个配置文件,回到你的Mod主类的构造方法中,添加
47 |
48 | ```java
49 | ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, Config.COMMON_CONFIG);
50 | ```
51 |
52 | Forge 提供了不同种类的配置文件,比如服务端起效,客户端起效的等,这里我们通过`ModConfig.Type.COMMON`选用了通用的配置文件。
53 |
54 | 到此你配置文件就已经注册完毕了。
55 |
56 | 你的配置文件的名字将会是`modid-common.toml`。
57 |
58 | 使用配置文件里的值,也是非常的简单。
59 |
60 | ```java
61 | public class ConfigureTestItem extends Item {
62 | public ConfigureTestItem() {
63 | super(new Properties().group(ModGroup.itemGroup));
64 | }
65 |
66 | @Override
67 | public ActionResult onItemRightClick(World worldIn, PlayerEntity playerIn, Hand handIn) {
68 | if (!worldIn.isRemote) {
69 | playerIn.sendMessage(new StringTextComponent(Integer.toString(Config.VALUE.get())));
70 | }
71 | return super.onItemRightClick(worldIn, playerIn, handIn);
72 | }
73 | }
74 | ```
75 |
76 | 直接通过`Config.VALUE.get()`就可以获取配置文件中的值了。
77 |
78 | 修改你的配置文件,你可以看到物品栏的消息也发生了改变。
79 |
80 | 
81 |
82 | 
83 |
84 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/configure)
85 |
86 |
--------------------------------------------------------------------------------
/src/gui/hud.md:
--------------------------------------------------------------------------------
1 | # HUD
2 |
3 | 在这节中我们将来学习如何绘制HUD。首先以免读者不清楚,HUD或者又称为inGameGui,指的是你在游戏中看到的类似于经验条、准星之类的东西。
4 |
5 | 那我们开始吧。
6 |
7 | Forge给我提供了一个事件让我们可以渲染HUD,这个事件是`RenderGameOverlayEvent` 。
8 |
9 | ```java
10 | @Mod.EventBusSubscriber(value = Dist.CLIENT)
11 | public class HudClientEvent {
12 |
13 | @SubscribeEvent
14 | public static void onOverlayRender(RenderGameOverlayEvent event) {
15 | if (event.getType() != RenderGameOverlayEvent.ElementType.ALL) {
16 | return;
17 | }
18 | if (Minecraft.getInstance().player == null || Minecraft.getInstance().player.getHeldItem(Hand.MAIN_HAND).getItem() != ItemRegistry.obsidianHud.get()) {
19 | return;
20 | }
21 | ObsidianGUI obsidianGUI = new ObsidianGUI();
22 | obsidianGUI.render();
23 | }
24 | }
25 | ```
26 |
27 | 可以看到,我们订阅了这个事件做了一些判断,最后调用`obsidianGUI.render();`进行了渲染。`RenderGameOverlayEvent`事件有`Pre`和`Post`两个子事件,大家可以按需选用。另外`RenderGameOverlayEvent`这个事件包含有不同的`ElementType`,在渲染你的内容前(特别是要渲染图片时),务必判断一次`ElementType`,以防导致原版内容的错误渲染,同样的这里别忘了`value = Dist.CLIENT`。
28 |
29 | ```java
30 | if (Minecraft.getInstance().player == null || Minecraft.getInstance().player.getHeldItem(Hand.MAIN_HAND).getItem() != ItemRegistry.obsidianHud.get()) {
31 | return;
32 | }
33 | ```
34 |
35 | 这里只是简单的判断玩家手上拿的东西是不是我们规定的物品,因为渲染肯定发生在客户端,所以这里我们调用`Minecraft.getInstance()`。
36 |
37 | 接下来我们来看`ObsidianGUI`的具体内容。
38 |
39 | ```java
40 | public class ObsidianGUI extends AbstractGui {
41 | private final int width;
42 | private final int height;
43 | private final Minecraft minecraft;
44 | private final ResourceLocation HUD = new ResourceLocation("neutrino", "textures/gui/hud.png");
45 |
46 | public ObsidianGUI() {
47 | this.width = Minecraft.getInstance().getMainWindow().getScaledWidth();
48 | this.height = Minecraft.getInstance().getMainWindow().getScaledHeight();
49 | this.minecraft = Minecraft.getInstance();
50 | }
51 |
52 | public void render() {
53 | RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
54 | this.minecraft.getTextureManager().bindTexture(HUD);
55 | blit(width / 2 - 16, height / 2 - 64, 0, 0, 32, 32, 32, 32);
56 | }
57 |
58 | }
59 | ```
60 |
61 | 可以看到我们继承了`AbstractGui`,这允许我们直接使用`AbstractGui`自带的一系列方法,比如`blit`。然后我们在构造方法里,手动的设置里类似于`width` `hegiht`等变量。
62 |
63 | 然后手动的创建了`render`方法,`render`方法的写法你之前在`Screen`中的`render`的写法是一样的。我在这里就只是渲染了一张图片而已。
64 |
65 | 打开游戏,把物品拿在手上,你应该就能看见物品被渲染出来了。
66 |
67 | 
68 |
69 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/hud)
70 |
71 | 另外原版的HUD渲染内容都在`IngameGui`这个类中。
72 |
73 |
--------------------------------------------------------------------------------
/src/tileentity/itickabletileentity.md:
--------------------------------------------------------------------------------
1 | # ITickableTileEntity
2 |
3 | 在这一节中,我们来学习TileEntity中最为重要的一个接口:`ITickableTileEntity`。
4 |
5 | 这一节我们将以制作一个会自动打招呼的方块为例,来体现这个接口的功能。
6 |
7 | `ObsidianHelloBlock`:
8 |
9 | ```java
10 | public class ObsidianHelloBlock extends Block {
11 | public ObsidianHelloBlock() {
12 | super(Properties.create(Material.ROCK).hardnessAndResistance(5));
13 | }
14 |
15 | @Override
16 | public boolean hasTileEntity(BlockState state) {
17 | return true;
18 | }
19 |
20 | @Nullable
21 | @Override
22 | public TileEntity createTileEntity(BlockState state, IBlockReader world) {
23 | return new ObsidianHelloTileEntity();
24 | }
25 | }
26 | ```
27 |
28 | `ObsidianHelloTileEntity`:
29 |
30 | ```java
31 | public class ObsidianHelloTileEntity extends TileEntity implements ITickableTileEntity {
32 | private static final int MAX_TIME = 5 * 20;
33 | private int timer = 0;
34 |
35 | public ObsidianHelloTileEntity() {
36 | super(TileEntityTypeRegistry.obsidianHelloTileentity.get());
37 | }
38 |
39 | @Override
40 | public void tick() {
41 | if (!world.isRemote) {
42 | if (timer == MAX_TIME) {
43 | PlayerEntity player = world.getClosestPlayer(pos.getX(), pos.getY(), pos.getZ(), 10, false);
44 | StringTextComponent stringTextComponent = new StringTextComponent("Hello");
45 | if(player!=null){
46 | player.sendMessage(stringTextComponent);
47 | }
48 | timer = 0;
49 | }
50 | timer++;
51 | }
52 | }
53 | }
54 | ```
55 |
56 | 实际上这个方块实体的代码要比我们做的第一个方块实体的代码简单许多。可以看到我们在这里实现了`ITickableTileEntity`接口,这个接口只有一个方法需要实现,就是`tick`方法,故名思义,这个会在每个游戏tick执行一次,我们这里做了一计数器,然后通过`world.getClosestPlayer`方法获取到了这个方块位置周围10格内最近的玩家,然后创建了`StringTextComponent`消息(~~之所以用这个消息的原因是因为我懒得写lang文件~~),最后调用了`player.sendMessage`将消息发送给了玩家,`sendMessage` 接受的是`ITextComponent`接口,minecraft已经有很多实现这个接口的常用的类。
57 |
58 | 注册`TileEntityType`:
59 |
60 | ```java
61 | public static RegistryObject> obsidianHelloTileentity = TILE_ENTITY_TYPE_DEFERRED_REGISTER.register("obsidian_hello_tileentity", () -> {
62 | return TileEntityType.Builder.create(() -> {
63 | return new ObsidianHelloTileEntity();},
64 | BlockRegistry.obsidianHelloBlock.get()).build(null);
65 | });
66 | ```
67 |
68 | 其他的注册和模型文件的创建这里就省略了,相信读者已经有能力自主完成。
69 |
70 | 打开游戏,靠近方块:
71 |
72 | 
73 |
74 | 可以看见我们的方块在和我们打招呼呢。
75 |
76 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/tickabletileentity)
77 |
78 | ## 开发小课堂
79 |
80 | 在很多时候,当函数要求传入的变量的类型是接口时,基本都已经有实现好的类可以使用,大家可以尝试查看这个接口的继承树来寻找可以使用的类。
--------------------------------------------------------------------------------
/src/item/meleeweapons.md:
--------------------------------------------------------------------------------
1 | # 近战武器
2 |
3 | 在这一节中,我们将讲解如何创建一个新的剑,这里我们以黑曜石剑举例。
4 |
5 | 同样的,我们先创建一个`ObsidianSword`,但是这次的继承的类有些不一样,这次我们直接继承原版的`SwordItem`类,如果你查看继承关系图,你就可以发现,`SwordItem`是`Item`的子类。
6 |
7 | 
8 |
9 | 内容如下:
10 |
11 | ```java
12 | public class ObsidianSword extends SwordItem {
13 | private static IItemTier iItemTier = new IItemTier() {
14 | @Override
15 | public int getMaxUses() {
16 | return 2000;
17 | }
18 |
19 | @Override
20 | public float getEfficiency() {
21 | return 10.0F;
22 | }
23 |
24 | @Override
25 | public float getAttackDamage() {
26 | return 4.0F;
27 | }
28 |
29 | @Override
30 | public int getHarvestLevel() {
31 | return 3;
32 | }
33 |
34 | @Override
35 | public int getEnchantability() {
36 | return 30;
37 | }
38 |
39 | @Override
40 | public Ingredient getRepairMaterial() {
41 | return Ingredient.fromItems(ItemRegistry.obsidianIngot.get());
42 | }
43 | };
44 |
45 | public ObsidianSword() {
46 | super(iItemTier, 3, -2.4F, new Item.Properties().group(ItemGroup.COMBAT));
47 | }
48 | }
49 | ```
50 |
51 | 同样的,这个内容看上去非常地多,但其实并没有你想象得那么复杂。
52 |
53 | 首先我们实现一个`IItemTier`接口的匿名内部类。首先什么是`IItemTier`呢?Tier的英文意思是「层、等级」,你可以把`IItemTier`理解成一种材质,比如钻石剑、钻石镐都是钻石做的,同样的,铁剑、铁镐都是铁做的。
54 |
55 | 那么为什么要自己实现这个匿名内部类呢?原因是原版的`net.minecraft.item.IItemTier`是用enum实现的,我们没法自己向里面添加内容,所以只能自己实现了,原版的所有属性也都在这个类里,大家可以参考。至于这个匿名内部类里的各种方法,我在这里就不多加解释了,有了之前几个物品的经验,相信读者阅读到这里时已经有了通过函数名猜测函数功能的能力了。关于构造函数里的`3`和`-2.4F`的作用也请读者参考原版物品的实现(原版所有物品的实例都写在`net.minecraft.item.Items`类中)猜测功能。
56 |
57 | 接下去注册物品
58 |
59 | ```java
60 | public static RegistryObject- obsidianSword = ITEMS.register("obsidian_sword", () -> {
61 | return new ObsidianSword();
62 | });
63 | ```
64 |
65 | 添加模型文件:
66 |
67 | ```json
68 | {
69 | "parent": "item/generated",
70 | "textures": {
71 | "layer0": "neutrino:item/obsidian_sword"
72 | }
73 | }
74 | ```
75 |
76 | 以及材质
77 |
78 |
79 |
80 | 创建完成之后打开游戏看看吧。
81 |
82 | 
83 |
84 | [源代码地址](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/melee_weapons)
85 |
86 | ## 开发小课堂
87 |
88 | 在开发的过程中你得熟练使用开发工具,在这里我们的工具是IntelliJ IDEA。有两个快捷键对于理解代码有非常大的帮助。第一个快捷键是`Ctrl+N(Windows)`,这个快捷键可以让你搜索指定的类,这样你就可寻找原版类里在哪里,有什么内容了。另一个快捷键就是`Ctrl+H`当你把鼠标指针放在一个类上时,按下这个快捷键,会在右侧显示这个类的继承关系,也可查看某个接口的具体实现,大家可以自己上网搜索IDEA常用快捷键学习使用。
89 |
90 | 另外一个技巧是,当你看到某个方法,想要知道这个方法在哪里调用时,可以右键然后点击`Find Usages(查找使用)`,你就可以看见所有调用这个方法的代码了。
91 |
92 | 还有如果你想查看某个类的源代码,只需要按住`Ctrl`键,点击那个类就可以进入到那个类内部查看它的源代码。
--------------------------------------------------------------------------------
/src/command/command.md:
--------------------------------------------------------------------------------
1 | # 命令
2 |
3 | 在这节中,我们将来学习如何向Minecraft中添加一个命令。首先要明确的是命令是一个服务端只在服务端存在的东西(虽然也有客户端的命令,但是我在写这段时[还没有实现](https://github.com/MinecraftForge/MinecraftForge/pull/6670))。
4 |
5 | 接下来开始我们的实现吧。
6 |
7 | ```
8 | /neurino test
9 | ```
10 |
11 | 这里我们以上面这条命令为例。
12 |
13 | ```java
14 | @Mod.EventBusSubscriber
15 | public class CommandEventHandler {
16 | @SubscribeEvent
17 | public static void onServerStaring(FMLServerStartingEvent event) {
18 | CommandDispatcher dispatcher = event.getCommandDispatcher();
19 | LiteralCommandNode cmd = dispatcher.register(
20 | Commands.literal("neutrino").then(
21 | Commands.literal("test")
22 | .requires((commandSource) -> {
23 | return commandSource.hasPermissionLevel(0);
24 | })
25 | .executes(TestCommand.instance)
26 | )
27 | );
28 | dispatcher.register(Commands.literal("nu").redirect(cmd));
29 | }
30 | }
31 | ```
32 |
33 | 可以看见,我们这里监听了`FMLServerStartingEvent`事件,你的命令注册将会在这里完成。
34 |
35 | 我们先不看内容,先来理解Minecraft是如何解析命令的。
36 |
37 | 在我们的例子中,Minecraft首先会根据预设试图解析有没有`neutrino`,如果有,接着解析在`neutrino`下有没有`test`命令,如果有就继续解析或者执行预设的程序,这里我们的命令只有两级,所以解析成功以后就直接运行预设的程序了。
38 |
39 | 而我们的代码也对应着这个解析过程,我们来仔细观察一下`dispatcher.register`中的代码。
40 |
41 | ```java
42 | Commands.literal("neutrino").then(
43 | Commands.literal("test")
44 | .requires((commandSource) -> {
45 | eturn commandSource.hasPermissionLevel(0);
46 | })
47 | .executes(TestCommand.instance)
48 | )
49 | ```
50 |
51 |
52 |
53 | `Commands.literal`代表着这是一个命令,具体来说是个没有参数的命令,这里命令就是`neutrino`。如果你需要有参数的命令`Commands`下还有别的类可以使用。`then`指定了,这个命令并没有到头,如果匹配了这个命令要继续接着解析。
54 |
55 | ` Commands.literal("test”)`这里就是上一条命令子命令的开始,这里的`requires`表面的执行权限是所有玩家都可以执行的。比起上一条命令,这里没有填入`then`方法,而是用了`executes`方法,这说明着我们的命令解析结束了。匹配成功之后就要自行`TestCommand.instance`这个实例所规定的操作。具体什么内容我们之后再看。
56 |
57 | ```java
58 | dispatcher.register(Commands.literal("nu").redirect(cmd));
59 | ```
60 |
61 | 而这条句话则是一个重定向,使得`neutrino`和`nu`等价。
62 |
63 | ```java
64 | public class TestCommand implements Command {
65 | public static TestCommand instance = new TestCommand();
66 |
67 | @Override
68 | public int run(CommandContext context) throws CommandSyntaxException {
69 | context.getSource().sendFeedback(new StringTextComponent("Hello,world"), false);
70 | return 0;
71 | }
72 | }
73 | ```
74 |
75 | 这就是我们具体要执行操作的类,可以看到我们继承了`Command`,它有个`run`需要实现,这个`run`方法里的内容就是,当你命令匹配成功时需要执行的东西。
76 |
77 | 这里我们就简单的实现了一个向玩家聊天框发送`Hello, World`的功能。
78 |
79 | 至此,我们的命令已经完成。打开游戏输入命令试试吧。
80 |
81 | 
82 |
83 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/command)
--------------------------------------------------------------------------------
/pic/GUi.drawio:
--------------------------------------------------------------------------------
1 | 7VvLdts2EP0aLJ0DEHwuSUlO0+O0PnWbpt3BIiwxgQiVgmKpX1+ABJ+gJCrRw0m9MjEEIXLuzJ3BYAzwaLF5m5Hl/D2PKQMWjDcAj4FlIcsN5B8l2WqJA1EhmWVJrGW14CH5l2oh1NJ1EtNVa6LgnIlk2RZOeZrSqWjJSJbx5/a0J87av7okM2oIHqaEmdI/k1jMC6lvebX8J5rM5uUvo/KLF6ScrL9kNScxf26I8ATgUca5KK4WmxFlSnulXornbnfcrV4so6kY8sC72fTXKfn8yz36NPuHz5cfsvefb5BdLPOFsLX+Yv22YluqQL74Ul0+MboJlUoBjmga68vxlJHVKplK4VwsmBQgeVl9LFQDQTJxm7Dypny4OYoTQR7zH1ND87P0l9K4hZT+yLeUL6jItnLCc42Po3U+b0BTyjLKiEi+tPEl2kxm1XLVL9zzRL6JBbVNY1evoy1aqhC211jxdTal+rEmIAdWst3OQlJtMyqMheRF47trUY73EdhbgQG10vGDHvJMzPmMp4RNammU8XUa01gjW8+543ypEfxEhdhqTyZrwduWIWHNth/V82+ccviXXi4fjDet0VaPdtpFoe5936lpI9fmnnn+QDsbbEAtwI5Gx3TMiQsCF0QBmDggGIPAUxfhBAQTMLFBJCXYdN7nZMFIqqB74qkowVUanc4TFt+RLV8rrUgnnX4uR9GcZ8m/cj6pHFr5sAYVw9aMB/WkXjOjKznnvkQJdUTvyaY18Y6sRPk2nDGyXCU1EywkYEkacSH4Qk8iLJmlinPkWjQrv6l4K0lle6zkC80E3ezFtbyL2m5ZemWDXpDbwy9d7z2ZJWDfMIXfE0YnqUjE9l36RX4qVz/jMoXcYyavZupK0obkaNJzx7ASqRrRdlND00qBiYyLob6xSOK4YASFpCZwhdFS8VWuAycCzlitJUlgpUHqxWivAxwGTgOFBwKFzwWUbcbOixJqi05rdt1BqE8yBI8441n+phg9EkSt3NMz/pk27kDoTsLbb6JgfyAFI+9FcbDpeCPFeEkqPeKVaE9NtMgf6MD+2Zg2GMK0D0ym7Aan3jOypdmBSd85JfuDIT6Skp1zIYpMF87TqBHwxyp78kMQIjDxQBgByXE7QGhTYsoLL2/wpxY1wGr4i2V3OAHtw85A+QyeZsMeXGAPLt2tzelwMT1NJ7NhDpDvg8j74XGxvQ4u6Nq4WP41UphOKhL7EHq4LxUJHQjtb9wNwoG5SBkMTpeL9JcBUNA2AssbVgbQgaSapkl+8O9g7HbMpVjxpDUGbF/Dnr7eOvBQ67BeVKJavnaDTiPGZbJ4iSzVcv9PWeqNYw3Maaxzge1ew6cuX7fDzkBfRM61nDFh2d8f/97+nP61RePn5dOd+H15g64KD3oDYRugN76M6vtBykf3NEukBpQj9RUIXkhUPnmVdkdx3upsTtF5ojJGe6PywflIG/95o7hnhBcwkRn6LQgCMAmA7wF/pNL2yMq3U8UGK8gvHBDBPKMfq+xeVa6hnhxKiWP6CmPJcqV8ojx3mjK+lm8XPc8TQR+WJDek54wsD2yPT0D+Duxou29Da1+0xnhl8u8wy9fwyjdEhKF1xDPl7sc6dyfjLrdyu3y7uxFwIDi7a9ve/yOZsNFA0yl5/4Vk9vbeGjQmcdxfaWxOEpnMyN9VpceDhcahZZXhBck+6m4b0QnI+qYbsh2TrP0errbOxtU9RS5jQ1YGuXXGtpECSlnnoVhX627vdvcY3eG28npq8bgv0KGzlW4dc09bmXldNS8Eat/X0qv7z5qXN26KQncoJyBvualvVquo9CRUB+gqqUG6IhyEwLf7UhgPRJE6ale3JvkcG0R+XlCWFyP1oFwntFTtUuVEvkqL1OTb/KlbNUdlQA7wb0EY5rd8VeVUP2qD0M8leXI0JIGaYCDB8q2+M4VCItVfKMgQPx5x/PDdsoK3I5NvmHbVbnaRImtZGegUv5U13OY2FJXYF1l0ZTF5k4fvNiymSrkr03Hy2jn8cQGtqEnj6ZlwepdkeQcfZvmj2uXoJhEfy0RJXuvjfIz1uM6N1GDbGHQz7SrNuoFvIHTBUf0Bvbl7TFbzKu50CgQQ+gTCvgIBhI4HL5T623hg/naZdkHUZZ+gY4dDuwWtbnbTpaev7hb8Y0WzXx8/qe5ZCzLySFnJSZ4KK0W/mZ9HPRWGCk4KQGDpaCjDkN7r4zIIjhV1mfFIPTVS0UrFR7kUUhdy5bCImKgOiwHKL+RkXSFYMskqc85imrU7elOy0Nh8IFmSH3rn8r0lOtwATPv4C6BKLmc/sZwH5nIeTU9DmLhjgrjv9LcvAH5FEUMOG+Z0zMGjme09TDMqdWBkzK9HGEcagN09+es7Zr7oEQYyC4qvnVXnA/z6nVVl+eUCnVUH897r91FV5v8dNVI5Zpb72o9+DWe+ej+6bW54juxH/x5c1BmM00tpP8emi742gZzHBy/aBNK7g7lKX1VZmSjrDY1Cwu66xI6jQ+tQ/WEnbAdrBWVz6eHGkZMfE34Tpm5PiK2KxHKjjPMStXmC7oI8pcdRk3xr2X7W7XefIR2xK7l7TdLZHX1SOsa15Ddt/2aziN4mZ1wQsZO0VfFypMxK0rc1kmNUj3NGX0rTldsFuTRJctugkjKe6UqcxtmxZx/MngPX9HXLPdou5LD+B+CiUlP/HzWe/Ac=
--------------------------------------------------------------------------------
/src/block/rendertype.md:
--------------------------------------------------------------------------------
1 | # 方块的渲染类型
2 |
3 | 在这一节中,我们将会聊一下渲染类型(RenderType)。
4 |
5 | 
6 |
7 | > 图片出自[TheGreyGhost博客](https://greyminecraftcoder.blogspot.com/2020/04/block-rendering-1144.html)
8 |
9 | 在Minecraft中,方块的渲染模型有四种,分别是`translucent`、`solid`、`cutout`和`cutout_mipped`。
10 |
11 | 大概的区别大家一样就能看出来。这里我们要详细讲一下`translucent`和`cutout*`之间的区别。它们的区别其实很简单,`translucent`是半透明,而`cutout*`是要不全透明,要不不透明。大家可以注意看这玻璃上的白色部分和冰块上白色部分的区别。
12 |
13 | 而`cutout`和`cutout_mipped`直接的区别涉及到游戏优化的地方,`cutout_mipped`是开启了Mipmapping的`cutout`。
14 |
15 | > 在三维计算机图形的贴图渲染中有一个常用的技术被称为**Mipmapping**。为了加快渲染速度和减少图像锯齿,贴图被处理成由一系列被预先计算和优化过的图片组成的文件,这样的贴图被称为 MIP map 或者 mipmap。这个技术在三维游戏中被非常广泛的使用。“MIP”来自于拉丁语 *multum in parvo* 的首字母,意思是“放置很多东西的小空间”。Mipmap 需要占用一定的内存空间,同时也遵循[小波压缩规则](https://zh.wikipedia.org/w/index.php?title=小波压缩规则&action=edit&redlink=1) (wavelet compression)。
16 | >
17 | > ——[Wikipedia](https://zh.wikipedia.org/wiki/Mipmap)
18 |
19 | 我们将以玻璃罐为例,来教大家如何设置方块的RenderType。
20 |
21 | 首先我们来创建方块,`GlassJar.java`:
22 |
23 | ```java
24 | public class GlassJar extends Block {
25 | public GlassJar() {
26 | super(Properties.create(Material.ROCK).hardnessAndResistance(5).notSolid());
27 | }
28 | }
29 | ```
30 |
31 | 当然别忘了注册。
32 |
33 | ```java
34 | public class BlockRegistry {
35 | public static final DeferredRegister BLOCKS = new DeferredRegister<>(ForgeRegistries.BLOCKS, "neutrino");
36 | public static final RegistryObject glassJar = BLOCKS.register("glass_jar", () -> {
37 | return new GlassJar();
38 | });
39 | }
40 | ```
41 |
42 | 接下来就是设置我们方块的RenderType的地方,`RenderTypeRegistry`:
43 |
44 | ```java
45 | @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD,value = Dist.CLIENT)
46 | public class RenderTypeRegistry {
47 | @SubscribeEvent
48 | public static void onRenderTypeSetup(FMLClientSetupEvent event) {
49 | RenderTypeLookup.setRenderLayer(BlockRegistry.glassJar.get(), RenderType.getTranslucent());
50 | }
51 | }
52 | ```
53 |
54 | 因为渲染相关的内容都是在客户端发生,所以我们在`FMLClientSetupEvent`事件下注册我们的RenderType。
55 |
56 | 物理服务器是没发设置`RenderType`的,为了物理服务器的兼容性,在这里我们添加了`value = Dist.CLIENT`使它只会在物理客户端上监听事件。Forge提供了很多和物理端打交道的东西,除了我们看到的`value = Dist.CLIENT`,还有`@OnlyIn`注释,这个加了这个注释之后你就可以指定一个类只存在于物理客户端,或者物理服务端。当然还有`DistExecutor`,这里类下面有很多方法用来在在不同的物理端来执行不同的代码。
57 |
58 | 一个非常常见的问题就是,在物理服务端调用了只有在物理客户端存在的类,比如加了`@OnlyIn(Dist.CLIENT)`的类或者方法,当你在物理服务端调用它时,它相当于是不存在的,这时就是出现错误。
59 |
60 | 你在注意你调用的代码究竟是处于哪一个「逻辑端」的同时,你也得注意你的代码就行执行在哪一个「物理端」。
61 |
62 | 这一切的努力都是为了在服务器上和在本地游戏中能共用一个jar文件。
63 |
64 | 这里我们调用了`RenderTypeLookup.setRenderLayer`来设置我们方块的RenderType,`RenderType.getTranslucent()`指定了我们的RenderType是`translucent`,在`RenderType`类下有着原版所有的RenderType,你也可以自定义RenderType从而使用自定义shader等高级功能。
65 |
66 | 具体的模型和材质请看Github。
67 |
68 | 打开游戏你就能看见我们的玻璃罐了。
69 |
70 | 
71 |
72 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/special_render_type)
73 |
74 |
--------------------------------------------------------------------------------
/src/specialmodel/obj.md:
--------------------------------------------------------------------------------
1 | # OBJ 模型
2 |
3 | 在这一节我我们将学习如何给方块添加OBJ物品模型,在开始添加OBJ模型之前强烈建议读者先阅读关于OBJ和MTL文件格式的定义以及相关名词的意义,这里有个简短的[说明](https://segmentfault.com/a/1190000021126476)
4 |
5 | 首先创建我们的方块`ObsidianOBJ.java`:
6 |
7 | ```java
8 | public class ObsidianOBJ extends Block {
9 | public ObsidianOBJ() {
10 | super(Properties.create(Material.ROCK).hardnessAndResistance(5).notSolid());
11 | }
12 | }
13 | ```
14 |
15 | 和之前创建的方法一致,因为我们创建的模型并不是实心的,所以加上了`notSolid`方法。
16 |
17 | 方块注册:
18 |
19 | ```java
20 | public static RegistryObject obsidanObj = BLOCKS.register("obsidian_obj", () -> {
21 | return new ObsidianOBJ();
22 | });
23 | ```
24 |
25 | 物品注册:
26 |
27 | ```java
28 | public static RegistryObject- obsidianObj = ITEMS.register("obsidian_obj", () -> {
29 | return new BlockItem(BlockRegistry.obsidanObj.get(), new Item.Properties().group(ModGroup.itemGroup));
30 | });
31 | ```
32 |
33 | 方块状态文件`obsidian_obj.json`
34 |
35 | ```json
36 | {
37 | "variants": {
38 | "": { "model": "neutrino:block/obsidian_obj" }
39 | }
40 | }
41 | ```
42 |
43 | 模型的Json模型文件`obsidian_obj.json`
44 |
45 | ```json
46 | {
47 | "loader": "forge:obj",
48 | "model": "neutrino:models/block/obsidian_obj.obj",
49 | "flip-v": true
50 | }
51 | ```
52 |
53 | 可以看到,从这里开始就有些特殊了,首先我们用`loader`指定了我们要加载的模型是`obj`格式的,然后在`model`里具体指定了我们的OBJ模型,最后将`flip-v`设置成为`true`,这么做的原因是minecraft里的材质和你在blender等工具里的材质是上下颠倒的,所以你得手动翻转你的材质。
54 |
55 | 接下来是OBJ模型`obsidian_obj.obj`,这里只标注需要修改的地方:
56 |
57 | ```
58 | mtllib obsidian_obj.mtl
59 | ```
60 |
61 | 你必须在这里指明你要使用的mtl文件的名字。
62 |
63 | 接下来是mtl文件``obsidian_obj.mtl`,同样的我在这里只标注需要修改的地方。
64 |
65 | ```
66 | map_Kd neutrino:block/obsidian_obj
67 | ```
68 |
69 | 你必须这样的方式来指定你模型文件的材质。
70 |
71 | 你可在这里获取[OBJ文件](obj.assets/obsidian_obj.obj)和[mtl文件](obj.assets/obsidian_obj.mtl)。
72 |
73 | 最后是我们的材质`obsidian_obj.png`:
74 |
75 |
76 |
77 |
78 |
79 | 
80 |
81 | 可以看到我们我们的OBJ模型已经成功加载出来了。当然我们在这里还没有设置正确的碰撞箱,这就交给读者自己实现了。
82 |
83 | 物品同样也是可以使用OBJ模型的,请读者自行探索。
84 |
85 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/obj)
86 |
87 | ---
88 |
89 | **常见坑的处理方法**
90 |
91 | ## 环境光遮蔽
92 |
93 | 在默认情况下,你可能会发现你的模型有着像下图一样不自然的黑色阴影,这是因为环境光遮蔽导致的,你可以通过复写`Block`类下的`getAmbientOcclusionLightValue`方法来修改方块的环境光遮蔽,其中默认是`0.2`,最大值为`1`,数值越大环境光遮蔽越小。
94 |
95 | 
96 |
97 | ## 夜晚不自然的高光
98 |
99 | 有时候你会发现你的模型在夜晚也会发出类似这样不自然的高光,这是由于`mtl`文件中多余的属性导致的,对于Mod开发建议只保留`map_Kd`属性,具体可以看IE的[mtl文件](https://github.com/BluSunrize/ImmersiveEngineering/blob/1.14/src/main/resources/assets/immersiveengineering/models/block/balloon.mtl)。
100 |
101 | 
102 |
103 | ---
104 |
105 | ## 开发小课堂
106 |
107 | 如果你在使用Blender制作OBJ模型,请将你的模型中心点设置为`X:0.5m,Y-0.5m,Z:0.5`,这样你就不需要在json文件中进行额外的偏移计算了。Minecraft一个满方块在Blender里刚好是`1m*1m*1m`。
108 |
109 |
--------------------------------------------------------------------------------
/src/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Summary
2 |
3 | - [导论](./introducation/intro.md)
4 | - [Forge是什么](./introducation/forge.md)
5 | - [Minecraft如何运作的](./introducation/vanilla.md)
6 | - [开发模型](./introducation/developmentmodel.md)
7 | - [一些核心概念](./introducation/coreconception.md)
8 | - [环境配置](./devenvironment/intro.md)
9 | - [Forge开发环境的配置](./devenvironment/setup.md)
10 | - [开发环境的介绍](./devenvironment/folderintro.md)
11 | - [自定义mod信息](./devenvironment/modinfo.md)
12 | - [物品](./item/intro.md)
13 | - [第一个物品](./item/firstitem.md)
14 | - [物品材质与模型](./item/modelandtextures.md)
15 | - [Item和ItemStack](./item/itemstack.md)
16 | - [食物](./item/food.md)
17 | - [近战武器](./item/meleeweapons.md)
18 | - [自定义创造模式物品栏](./item/group.md)
19 | - [语言文件与本地化](./i18n/i18n.md)
20 | - [方块](./block/intro.md)
21 | - [第一个方块](./block/firstblock.md)
22 | - [Block和BlockState](./block/blockandblockstate.md)
23 | - [方块模型和材质](./block/modelandtextures.md)
24 | - [方块状态](./block/blocksstates.md)
25 | - [非实心方块与自定义模型](./block/nonesoildblock.md)
26 | - [方块的渲染类型](./block/rendertype.md)
27 | - [特殊模型](./specialmodel/intro.md)
28 | - [OBJ 模型](./specialmodel/obj.md)
29 | - [B3D 模型](./specialmodel/b3d.md)
30 | - [动画](./specialmodel/animation.md)
31 | - [方块实体](./tileentity/intro.md)
32 | - [第一个方块实体和其数据保存](./tileentity/firsttileentity.md)
33 | - [ITickableTileEntity](./tileentity/itickabletileentity.md)
34 | - [方块实体内置的数据同步](./tileentity/datasync.md)
35 | - [特殊渲染](./specialrender/intro.md)
36 | - [IBakedModel(烘培模型)](./specialrender/ibakedmodel.md)
37 | - [TileEntityRenderer(方块实体渲染器)](./specialrender/ter.md)
38 | - [ItemStackTileEntityRenderer(物品特殊渲染)](./specialrender/ister.md)
39 | - [事件系统](./event/intro.md)
40 | - [网络包](./networking/intro.md)
41 | - [自定义网络包](./networking/custompack.md)
42 | - [关于Mod安全](./networking/secure.md)
43 | - [实体](./entity/intro.md)
44 | - [从零构建一个实体和数据同步](./entity/scratchentity.md)
45 | - [创建一个动物和AI](./entity/passiveentityandai.md)
46 | - [能力系统](./capability/intro.md)
47 | - [从零构建与使用能力](./capability/capabilityfromscratch.md)
48 | - [开始使用预定义能力](./capability/simpleusage.md)
49 | - [附加能力提供器](./capability/attachcapabilityprovider.md)
50 | - [WorldSavedData(世界数据保存)](./worldsaveddata/example.md)
51 | - [Gui](./gui/intro.md)
52 | - [第一个Gui](./gui/firstgui.md)
53 | - [Container](./gui/container.md)
54 | - [HUD](./gui/hud.md)
55 | - [流体](./fluid/firstfluid.md)
56 | - [世界生成](./worldgeneration/intro.md)
57 | - [矿物生成](./worldgeneration/oregeneration.md)
58 | - [结构生成](./worldgeneration/structuregeneration.md)
59 | - [自定义生物群系和世界类型](./worldgeneration/biomegenerationandworldtype.md)
60 | - [自定义维度与区块生成器以及生物群系提供器](./worldgeneration/dimensionenerationandchunkgeneratorandbiomeprovider.md)
61 | - [数据包](./datapack/intro.md)
62 | - [配方](./datapack/recipes.md)
63 | - [掉落物配方](./datapack/lootable.md)
64 | - [Data Generator](./datagnerator/intro.md)
65 | - [命令](./command/command.md)
66 | - [进度](./advancements/intro.md)
67 | - [配置文件](./configure/configure.md)
68 | - [药水](./potion/intro.md)
69 | - [粒子效果](./paticles/intro.md)
70 | - [音效](./sounds/intro.md)
71 | - [用户输入](./io/intro.md)
72 | - [与其他mod的兼容](./compatible/intro.md)
73 | - [Access Transformer](./at/intro.md)
74 | - [CoreMod](./coremode/intro.md)
75 |
--------------------------------------------------------------------------------
/src/block/firstblock.md:
--------------------------------------------------------------------------------
1 | # 第一个方块
2 |
3 | 在这一节中,我们将会创建一个最简单的方块,一个什么功能也没有的方块,甚至连模型和材质也没有。在开始之前我必须做一个概念上的区分,关于物品和方块概念上的区分。任何你可在拿在手中的东西都是「物品」,只有放置在世界中才成为了「方块」。
4 |
5 | 
6 |
7 | 这个是一个物品
8 |
9 | 
10 |
11 | 这个才是一个方块。
12 |
13 | 好的,我们已经把物品和方块之间的区别说清楚了。接下来就开始创建第一个方块吧,这里我们以黑曜石块为例。
14 |
15 | 首先创建一个类,叫做`ObsidianBlock`,就和所有的自定义物品需要继承`Item`类一样,所有的自定义方法都需要继承`Block`类,请确保你的`Block`类是`net.minecraft.block.Block`这个类。
16 |
17 | 具体内容如下:
18 |
19 | ```java
20 | public class ObsidianBlock extends Block {
21 | public ObsidianBlock() {
22 | super(Properties.create(Material.ROCK).hardnessAndResistance(5));
23 | }
24 | }
25 | ```
26 |
27 | 内容非常简单,就和`Item`有`Properties`一样,方块也需要一个`Properties`,和物品的`Properties`不太一样的是,方块的`Propeteis`需要调用`create`方法创建。请注意虽然这两个`Properties`名字相同,但是它们不在同一个包内,这其实是两个不同的类,`create`方法需要一个参数,这个参数是一个`Material`(材料),`Material`帮你预设了一些方块的特质,比如是否是实心的,是否是流体等,你可以通过调用`Properties`的方法来覆盖`Material`带来的特征,这里我们用的是原版的`Material.ROCK`,如果你想自己创建一个`Material`也非常简单,请参考原版的实现。最后我们调用`hardnessAndResistance`方法来为我的方块设置硬度。
28 |
29 | 至此我们的黑曜石方块类已经创建完毕。就如物品需要注册一样,我的方块也需要注册。
30 |
31 | 创建一个新的注册类`BlockRegistry`,内容如下:
32 |
33 | ```java
34 | public class BlockRegistry {
35 | public static final DeferredRegister BLOCKS = new DeferredRegister<>(ForgeRegistries.BLOCKS, "neutrino");
36 | public static RegistryObject obsidianBlock = BLOCKS.register("obsidian_block", () -> {
37 | return new ObsidianBlock();
38 | });
39 | }
40 | ```
41 |
42 | 相信之前已经写过注册类的你对这些内容应该相对熟悉了。我们把`DeferredRegister- `换成了`DeferredRegister`(当然后面的实例化参数也需要修改),这样我们就创建了一个方块的注册器。注册方式也和物品注册如出一辙,这里就不多加阐述了。
43 |
44 | 你还需要在你的主类构造方法里,将这个注册器注册到`mod`总线中。
45 |
46 | ```java
47 | @Mod("neutrino")
48 | public class Neutrino {
49 | public Neutrino() {
50 | ItemRegistry.ITEMS.register(FMLJavaModLoadingContext.get().getModEventBus());
51 | BlockRegistry.BLOCKS.register(FMLJavaModLoadingContext.get().getModEventBus());
52 | }
53 | }
54 | ```
55 |
56 | 到此,我们就已经完成了我们的方块,现在启动游戏,输入如下命令,你就可以放置你自己的方块了。
57 |
58 | ```
59 | /setblock ~ ~ ~ <你的modID>:<你的方块的注册名>
60 | 以我们的例子来说
61 | /setblock ~ ~ ~ neutriono:obsidian_block
62 | ```
63 |
64 | 
65 |
66 | 但是,只能通过命令放置的方块显然不符合我们的胃口,我们希望有一个物品可以和方块相对应,这样我们就不需要使用命令放置方块了,对于这个常见的需求,Minecraft 也提供了一个方便的类来满足,这个类就是`BlockItem`,我们只需要创建并实例化这个类就行了。
67 |
68 | 我们回到我们的`ItemRegistry`,添加如下一行
69 |
70 | ```java
71 | public static RegistryObject
- obsidianBlock = ITEMS.register("obsidian_block", () -> {
72 | return new BlockItem(BlockRegistry.obsidianBlock.get(), new Item.Properties().group(ModGroup.itemGroup));
73 | });
74 | ```
75 |
76 | 可以看见我们创建了一个BlockItem的实例,它的构造方法需要两个参数,第一个参数是你注册好的方块的实例,我们可以通过`BlockRegistry.obsidianBlock.get()`获取到之前注册好的方块实例,第二个参数是一个Item的`Properties`,这里的`Properties`非常简单,我们将其添加到我们之前创建的创造模式物品栏里。
77 |
78 | 此时打开游戏,你就可以在我们的创造模式物品栏里看见我们的方块了。
79 |
80 | 
81 |
82 | 
83 |
84 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/first_block)
--------------------------------------------------------------------------------
/src/item/firstitem.md:
--------------------------------------------------------------------------------
1 | # 第一个物品
2 |
3 | 从现在开始我们就要正式开始写代码了。首先有几件事要说明,本项目的代码都会开源,每节的代码链接我都会放在文章的后面,而为了之后修正和查看方便,**我的代码中可能会有大量重复的类,项目的组织也和正常项目大相径庭,大家请不要照抄,请大家务必手打代码,你复制粘贴代码是学不会的。**
4 |
5 | 首先我们得明确,创建一个物品需要哪几个步骤。答案是三步:创建自己的物品并继承原版的物品的类,实例化这个物品,最后把这个物品注册进游戏。
6 |
7 | 注意上面这些步骤是通用的,很多自定义内容的添加都是遵循着上面的步骤。
8 |
9 | 知道了上述步骤之后,我们就开始添加我们的第一个物品吧,在这里我们将添加一个黑曜石碇。
10 |
11 | 首先我们需要创建一个物品的类,并且让这个类继承原版的`Item`类(介于目标读者的水平,Item就是物品的英文)。
12 |
13 | ```java
14 | public class ObsidianIngot extends Item {
15 | public ObsidianIngot() {
16 | super(new Properties().group(ItemGroup.MATERIALS));
17 | }
18 | }
19 | ```
20 |
21 | 这个类的代码非常简单,只有一个构造函数。
22 |
23 | 这里唯一值得一说的就是`new Properties().group(ItemGroup.MATERIALS)`,这个`Properties`规定了物品的一些属性,比如:是不是食物,或者这个物品在创造模式的哪一个物品栏。
24 |
25 | 在这里我们创建了一个`Properties`并且调用了`group`方法然后传入了`ItemGroup.MATERIALS`,这样做是将物品添加进,原版「杂项」创造模式物品栏里。当然你也可以不调用 `group`方法,如果这样就只能通过`/give`命令才能获取到物品了。
26 |
27 | 接下去我们需要实例化和注册这个物品,在以前这个是分开的两步,但是Forge加入了一个叫做`DeferredRegister`的机制,使得注册一个物品变得非常的简单。
28 |
29 | ```java
30 | public class ItemRegistry {
31 | public static final DeferredRegister
- ITEMS = new DeferredRegister<>(ForgeRegistries.ITEMS, "neutrino");
32 | public static RegistryObject
- obsidianIngot = ITEMS.register("obsidian_ingot", () -> {
33 | return new ObsidianIngot();
34 | });
35 | }
36 | ```
37 |
38 | 这就是注册的全部内容,首先我们创建了一个类型为`DeferredRegister
- `名字叫做`ITEMS`的变量,这个泛型表明我们需要注册的东西是物品,然后通过`new DeferredRegister<>(ForgeRegistries.ITEMS, "neutrino”);`实例化了这个类,这个类里有两个参数`ForgeRegistries.ITEMS`代表了我们要注册的是物品,第二个参数填入的应该是你的`modId`。这样我们就创建好了注册器,接下去就是注册我们的物品。
39 |
40 | 还记得我之前说过的吗?注册需要两个东西,一个是「注册名」,还有一个就是你要注册对象的实例,`ITEMS.register`里的两个参数就是分别对应了这两个东西。
41 |
42 | ```java
43 | public static RegistryObject
- obsidianIngot = ITEMS.register("obsidian_ingot", () -> {
44 | return new ObsidianIngot();
45 | });
46 | ```
47 |
48 | 第一个参数很好理解,`”obsidian_ingot”`就对应着注册名,请注意这里的注册名也不要用大写字母,第二个参数看上去非常复杂,但是实际上也挺简单的。
49 |
50 | 首先我们先来看一下`ITEMS`的`register`的函数签名。
51 |
52 | ```java
53 | public RegistryObject register(final String name, final Supplier extends I> sup)
54 | ```
55 |
56 | 从这个函数签名里可以看见,第二个参数的类型是`Supplier extends I>`,这意味着你要传入的是一个没有参数的「匿名函数」,而且这个「匿名函数」是得有返回值的。
57 |
58 | 那么什么是「匿名函数」呢?
59 |
60 | ```java
61 | () -> {
62 | return new ObsidianIngot();
63 | }
64 | ```
65 |
66 | 这个就是一个匿名函数,当然这是一个没有传入参数而且有返回值的匿名函数,匿名函数也可以有参数,也可不需要返回值。
67 |
68 | ```java
69 | (T) ->{
70 | System.out.println(T);
71 | }
72 | //这只是演示代码
73 | ```
74 |
75 | 上面这个「匿名函数」就是传入一个参数,没有返回值的「匿名函数」。
76 |
77 | 回到我们的mod开发。
78 |
79 | ```java
80 | () -> {
81 | return new ObsidianIngot();
82 | }
83 | ```
84 |
85 | 这个匿名函数的意思就是返回一个我们之前创建的黑曜石类的实例。
86 |
87 | 你看,虽然我们没有显式声明变量,但是我们还是在注册时实例化了我们物品的类。
88 |
89 | 还差最后一步,我们就可以成功添加物品了。
90 |
91 | ```java
92 | @Mod("neutrino")
93 | public class Neutrino {
94 | public Neutrino() {
95 | ItemRegistry.ITEMS.register(FMLJavaModLoadingContext.get().getModEventBus());
96 | }
97 | }
98 | ```
99 |
100 | 我们在`Mod`主类的构建方法里添加了一行代码,`FMLJavaModLoadingContext.get().getModEventBus()`这句话的意思是获取`Mod`总线,如果你不知什么是`Mod`总线请向前翻阅。而`ITEMS.register(FMLJavaModLoadingContext.get().getModEventBus());`的意思就是将`ITEMS`注册进`Mod`总线里。为什么要注册进`Mod`总线里呢?原因是,`DeferredRegister`是基于事件系统实现的。
101 |
102 | 到这里,我们要添加的物品所需的代码已经写完了,打开游戏看看吧。
103 |
104 | 
105 |
106 | 
107 |
108 | 
109 |
110 | 虽然这个物品还是很丑,但这就是我们第一个物品了。
111 |
112 | [源码链接](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/first_item)
113 |
114 | ## 开发小课堂
115 |
116 | 在开发大型项目的过程中,通过看函数名猜函数功能,这是一个非常重要的能力。当你不清楚一个函数的功能是什么,就翻译翻译它的名字,猜猜看它的功能吧。
117 |
118 | 另外一个就是要学会看函数签名,理解一个函数最重要一点就是要理解它的输入是什么(参数),它的输出是什么(返回值)。
--------------------------------------------------------------------------------
/src/devenvironment/modinfo.md:
--------------------------------------------------------------------------------
1 | # 自定义mod信息
2 |
3 | 从这节起我们就会开始正式的写我们mod了!
4 |
5 | ## 更新Mappings
6 |
7 | 在开始之前我们需要更新一下Mapping文件,如果不知道Mapping文件的作用,请向前翻阅。再提醒一次,你可以在[这里](http://export.mcpbot.bspk.rs/)找到最新的mapping文件。
8 |
9 | **请确保你的mappings文件版本新于`20200512-1.15.1`**。
10 |
11 | 
12 |
13 | 于是我们需要将`build.gradle`下的`mappings channel: 'snapshot', version: '20190719-1.14.3’`修改为`mappings channel: 'snapshot', version: '20200512-1.15.1’`。
14 |
15 | 
16 |
17 | 然后点击右侧Gradle面板的重新导入按钮,重新导入项目,因为`build.gradle`文件非常的重要,请注意不要改错。
18 |
19 | 
20 |
21 | 这个过程可能会涉及下载文件(但不会很多),有出现错误的可能性,出错了请检查你的`build.gradle`内容有没有填错,然后多试几次。
22 |
23 | ## 配置
24 |
25 | 首先我们选中`java`文件夹下所有的目录和文件,然后右键删除Java包下的默认类。
26 |
27 | 
28 |
29 | 然后再右键新建立一个包
30 |
31 | 
32 |
33 | 在默认情况下你的包名应该是你的域名的倒写,因为我不想用自己的域名举例子,所以这里我填入的内容是`com.tutorial.neutrino`。
34 |
35 | 创建完成以后右击创建一个Java类,名字叫做`Neutrino`,请注意大小写,在默认情况下Java的类名遵循「帕斯卡命名法」[^1]。
36 |
37 | 
38 |
39 | 
40 |
41 | 创建完成后目录树如下:
42 |
43 | ```
44 | java
45 | └── com
46 | └── tutorial
47 | └── neutrino
48 | └── Neutrino.java
49 | ```
50 |
51 | 然后进入`Neutrino`在类名的上方添加一个`@Mod()`注解,其中填入的参数是你的`modId`,那么什么是你的`modId`呢?`modId`就是你mod名字的唯一标识符,请注意`modId`和你的mod名字不是同一个东西,它不允许大写字母,也不允许空格等内容。在这里我们选用的`modId`是`neutrino`。添加完成后内容如下:
52 |
53 | ```java
54 | @Mod("neutrino")
55 | public class Neutrino {
56 | }
57 | ```
58 |
59 | 接下来我们需要去修改处于`resources=>META-INF`下的`mods.toml`。在默认情况下IntelliJ是没有对Toml文件语法高亮的,如果你需要像我一样的语法高亮可以去安装一个`Toml`插件,安装方式和你安装语言包是一样的。
60 |
61 | `mods.toml`是我们mod信息的配置文件,在这里我们可以修改我们mod的名字,介绍等内容。其中有许多配置项,如果一个配置项的注释里含有`#mandatory`说明这个配置项是必须的,如果注射里写的的是`#optional`,说明这个配置项是可选的,你可以在配置项前面加上`#`来注释掉这个配置项。
62 |
63 | | 配置项 | 作用 |
64 | | :-------------: | :-------------------------------------------------: |
65 | | modLoader | 规定mod的Loader,大部分情况下不需要修改 |
66 | | loaderVersion | 规定了mod运行的Forge版本,大部分情况下不需要修改 |
67 | | issueTrackerURL | 可选,你的Mod Bug提交地址,按需修改 |
68 | | modId | 必填,这里需要填入你的`modId`,和代码中的要保持一致 |
69 | | version | 必填,一般情况下保持默认即可 |
70 | | displayName | 必填,显示名称,你的mod在Mod界面的显示名称 |
71 | | updateJSONURL | 可选,你的mod的更新链接 |
72 | | displayURL | 可选,你的mod介绍网页的链接 |
73 | | logoFile | 可选,你的Mod的Logo |
74 | | credits | 可选,你的Mod的致谢名单 |
75 | | authors | 可选,你的mod的作者名单 |
76 | | description | 必填,你的mod在mod界面的介绍 |
77 |
78 | 接下剩下的都是依赖,Forge官方的例子已经写的很清楚了,这里我们不多加说明
79 |
80 | 我修改完的`mods.toml`如下:
81 |
82 | ```toml
83 | modLoader="javafml"
84 | loaderVersion="[31,)"
85 | [[mods]] #mandatory
86 | modId="neutrino"
87 | version="${file.jarVersion}"
88 | displayName="Neutrino"
89 | authors="FledgeShiu"
90 | description='''
91 | 这是我们的教学mod!
92 | '''
93 | ```
94 |
95 | 现在我们已经修改完我们的mod信息了,现在让我打开游戏。
96 |
97 | 
98 |
99 | 可以看见我们的Mod已经出现了!
100 |
101 | ## 开发小课堂
102 |
103 | 介于目标读者的水平,在这里特增「开发小课堂」环节。
104 |
105 | 在这次开发小课堂中,我们来讲一讲「日志」,日志你在开发过程中最重要的Debug工具之一,在向别人提问时,你需要提供的三样东西中的其中一项,另外两项是完整的代码和问题清晰到位的问题描述。
106 |
107 | 如果代码出了问题,你首先需要做的事就是阅读日志,什么你说全英文的看不懂怎么办?现在这么多翻译网站随便找一个把日志粘贴上去翻译啊。
108 |
109 | 如果你问文件形式的日志在哪里,是你开发目录下的`run/logs/latest.log`。
110 |
111 | ---
112 |
113 | [^1]:「帕斯卡命名法」的意思是:名字中所有的单词首字母都是大写,比如「HelloWorld」
--------------------------------------------------------------------------------
/src/devenvironment/setup.md:
--------------------------------------------------------------------------------
1 | # Forge开发环境的配置
2 |
3 | ## 需要的工具
4 |
5 | - [AdoptOpenJDK8-HotSpot](https://adoptopenjdk.net/?variant=openjdk8&jvmVariant=hotspot),出于兼容性的考虑,**请确保你安装的是JDK8**。
6 | - [IntelliJ IDEA 2020.1.1 社区版](https://www.jetbrains.com/zh-cn/idea/download/),下载完成后请自行安装,介于目标读者的水平,这里有个如何给2020.1之后的版本安装官方中文的[教程](https://www.bilibili.com/video/BV1NT4y137nb)。
7 |
8 | - [Forge MDK 1.15.2 - 31.2.0](https://files.minecraftforge.net/maven/net/minecraftforge/forge/1.15.2-31.2.0/forge-1.15.2-31.2.0-mdk.zip),下载后请解压到你喜欢的文件夹,请注意这里的解压文件夹不要包括任何的中文、空格以及一些特殊符号(比如「!」)。
9 |
10 | **注意,介于预想读者的水平,配置过程十有八九是会失败的,建议直接使用[ForgeGradleCN(推荐)](https://v2mcdev.com/t/topic/589)或者[离线包](https://v2mcdev.com/t/topic/249/2),配置完ForgeGradleCN后继续进行之后的步骤**
11 |
12 | ## 总体的介绍
13 |
14 | Minecraft Forge是一个Gradle项目,Gradle是一个项目构建工具,其主要作用是负责项目的依赖管理、构建等功能。依赖管理指的是帮你自动地下载和配置你开发中使用的库,也就是别人写好的方便你自己开发的代码。构建指的是将你写的mod打包成别人可以安装的jar文件。
15 |
16 | Forge官方写了一个叫做ForgeGradle(以后简称FG)的插件来负责整个mod开发环境的配置(~~为什么要说这个呢,让你知道当环境配置失败时该骂谁~~)。
17 |
18 | ## 开始配置
19 |
20 | 首先选择启动页面的`打开或导入`。
21 |
22 | 
23 |
24 | 选择你MDK解压目录下的`build.gradle`打开。
25 |
26 | 
27 |
28 | 选择作为`项目打开`
29 |
30 | 
31 |
32 | 打开之后,根据你网络和自身电脑的情况,会有或长或短的导入时间,这个过程需要下载很多的依赖包,而这些依赖包都存放在海外,介于中国大陆网络封锁,导致海外网络访问不稳定,这个时间将会持续几分钟至几天不等,而且很有可能失败,对于有代理的同学可以自行搜索「Gradle配置代理」来给Gradle加上代理。
33 |
34 | 当导入结束,点击下方的`build`面板,左侧显示绿勾时说明导入成功。
35 |
36 | 
37 |
38 | 当导入完成后,点击运行右侧的`Gradle`面板,选择其中的`Tasks`下`fg_runs`下的`genIntelliJRuns`。
39 |
40 | 在这一步中,会自动下载剩余的一些依赖,以及Minecraft的资源文件。出于和上面相同的理由,这个过程耗时会很长,并且非常容易失败。
41 |
42 | 同样的当左侧显示「绿勾」时说明配置成功。
43 |
44 | 
45 |
46 | 点击上方的`运行=>编辑配置`。
47 |
48 | 
49 |
50 | 选择`应用程序`下的三项,将其`使用模块的类路径`改成`你文件夹名.main`,点击确定保存。
51 |
52 | 
53 |
54 | 配置完成之后,选择`调试`。
55 |
56 | 
57 |
58 | 然后选择`runClient`即可启动游戏。
59 |
60 | 
61 |
62 | 在没有为Gradle配置代理的情况下,`runClient`有时候会耗费非常多的时间,推荐大家购买并配置代理,如果没有代理可以按照[这个教程](https://v2mcdev.com/t/topic/304)解决。
63 |
64 | 可以看见我们的游戏成功启动了。
65 |
66 | 
67 |
68 | 如果大家嫌`控制台`输出太多,可以在`虚拟机选项`中将`-Dforge.logging.console.level=`的值改成`info`。
69 |
70 | 
71 |
72 | 为了之后创建目录和子包的方便,按照下图将「拼合包」和「压缩空的中间包」取消选择。
73 |
74 | 
75 |
76 | ## IDEA的编码配置(Windows用户专用)
77 |
78 | 选择Configure下的`设置/首选项`。
79 |
80 | 
81 |
82 |
83 |
84 | 选择`编辑器=>文件编码`,将右侧所有涉及到编码全部改成`UTF-8`,并把`创建UTF-8文件`改成`With NO BOM`。
85 |
86 | 
87 |
88 | ## JDK常见错误
89 |
90 | 如果你的电脑里有多个JDK,有可能IntelliJ自动选择的JDK是错误的,导致无法导入,你需要手动修改项目的JDK和Gradle运行所需要的JDK。
91 |
92 | 选择`文件`下的`项目结构`。
93 |
94 | 
95 |
96 | 将项目JDK改成1.8版本
97 |
98 | 
99 |
100 | 接下去修改gradle的版本,出于一些奇怪的原因,在装了中文插件之后,就无法修改gradle的JDK了,所以首先你得先在设置面板停用中文插件。
101 |
102 | 
103 |
104 | 然后按照下图,到设置面板,将`Gradle VM`改成`Project JDK`
105 |
106 | 
107 |
108 | 之后再到`Plugin`下的`Installed`重新启动中文插件即可。
109 |
110 | 
111 |
112 |
--------------------------------------------------------------------------------
/src/block/blocksstates.md:
--------------------------------------------------------------------------------
1 | # 方块状态
2 |
3 | 在之前我们已经稍微地提及了BlockState,但是我们的第一个方块显然是没有状态的,这一节我们将以黑曜石魔方举例来创建一个带有状态的方块。
4 |
5 | 首先创建一个叫做`ObsidianRubikCube`的类,内容如下:
6 |
7 | ```java
8 | public class ObsidianRubikCube extends Block {
9 | private static IntegerProperty STATE = IntegerProperty.create("face", 0, 1);
10 |
11 | public ObsidianRubikCube() {
12 | super(Properties.create(Material.ROCK).hardnessAndResistance(5));
13 | this.setDefaultState(this.stateContainer.getBaseState().with(STATE, 1));
14 | }
15 |
16 | @Override
17 | protected void fillStateContainer(StateContainer.Builder builder) {
18 | builder.add(STATE);
19 | super.fillStateContainer(builder);
20 | }
21 | }
22 | ```
23 |
24 | 这里有三个和之前我创建方块不一样的地方,首先就是:
25 |
26 | ```java
27 | private static IntegerProperty STATE = IntegerProperty.create("face", 0, 1);
28 | ```
29 |
30 | 在这句话里,我们创建了一个新的方块状态,正如`IntegerProperty`这个名字暗示的那样,这个是一个整数类型的方块状态,除了`IntegerProperty`原版还实现了`BooleanProperty`和`EnumProperty`,并且原版还在`BlockStateProperties`类下实现了很多预设的方块状态,可以按需使用。如果这些类型都不满足你的需求,你还可以继承`Property`自己创建一个新的种类的方块状态。
31 |
32 | ```java
33 | IntegerProperty.create("face", 0, 1)
34 | ```
35 |
36 | 的意思是,这个方块状态的名字叫做`face`,最小值是`0`,最大值是`1`。
37 |
38 | ```java
39 | @Override
40 | protected void fillStateContainer(StateContainer.Builder builder) {
41 | builder.add(STATE);
42 | super.fillStateContainer(builder);
43 | }
44 | ```
45 |
46 | 然后我们在`fillStateContainer`里调用传入的`builder`变量中的`add`方法给我的方块添加了一个状态。
47 |
48 | 最后,我们在构造方法里设置了默认状态(可以不用设置)。
49 |
50 | ```java
51 | this.setDefaultState(this.stateContainer.getBaseState().with(STATE, 1));
52 | ```
53 |
54 | 注册方块
55 |
56 | ```java
57 | public static RegistryObject obsidianRubikCube = BLOCKS.register("obsidian_rubik_cube", () -> {
58 | return new ObsidianRubikCube();
59 | });
60 | ```
61 |
62 | 注册物品
63 |
64 | ```java
65 | public static RegistryObject
- obsidianRubikCube = ITEMS.register("obsidian_rubik_cube", () -> {
66 | return new BlockItem(BlockRegistry.obsidianRubikCube.get(), new Item.Properties().group(ModGroup.itemGroup));
67 | });
68 | ```
69 |
70 | 接下来在`blockstates`文件夹下创建你和方块注册名相同的json文件,我们创建`obsidian_rubik_cube.json`,内容如下:
71 |
72 | ```json
73 | {
74 | "variants": {
75 | "face=0": { "model": "neutrino:block/obsidian_rubik_cube_model_0" },
76 | "face=1": { "model": "neutrino:block/obsidian_rubik_cube_model_1" }
77 | }
78 | }
79 | ```
80 |
81 | 可以看到,我们在这里为不同的`face`值指定了不同的模型,分别是`obsidian_rubik_cube_model_0`和`obsidian_rubik_cube_model_1`。请注意,如果你要定义多个blockstate的值,请用半角逗号隔开,中间不要有空格。具体的要求也请参考Wiki中关于[模型](https://minecraft-zh.gamepedia.com/index.php?title=%E6%A8%A1%E5%9E%8B&variant=zh)的章节。
82 |
83 | 然后我们在`models/block`下创建`obsidian_rubik_cube_model_0.json`和`obsidian_rubik_cube_model_1.json`这两个模型文件。
84 |
85 | `obsidian_rubik_cube_model_0.json`:
86 |
87 | ```json
88 | {
89 | "parent": "block/cube_all",
90 | "textures": {
91 | "all": "neutrino:block/obsidian_rubik_cube_texture_0"
92 | }
93 | }
94 | ```
95 |
96 | `obsidian_rubik_cube_model_1.json`:
97 |
98 | ```json
99 | {
100 | "parent": "block/cube_all",
101 | "textures": {
102 | "all": "neutrino:block/obsidian_rubik_cube_texture_1"
103 | }
104 | }
105 | ```
106 |
107 | 可以看见它分别加载了两个不同的材质。
108 |
109 | 然后添加材质。
110 |
111 | `obsidian_rubik_cube_texture_0.png`
112 |
113 |
114 |
115 | `obsidian_rubik_cube_texture_1.png`
116 |
117 |
118 |
119 | 最后给我们的物品模型`obsidian_rubik_cube`添加内容:
120 |
121 | ```json
122 | {
123 | "parent": "neutrino:block/obsidian_rubik_cube_model_1"
124 | }
125 | ```
126 |
127 | 
128 |
129 | 
130 |
131 | 可以看到,随着我们用debug stick改变了方块的状态,方块的模型和材质也发生了改变。
132 |
133 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/blockstate)
134 |
135 |
--------------------------------------------------------------------------------
/src/worldgeneration/biomegenerationandworldtype.md:
--------------------------------------------------------------------------------
1 | # 自定义生物群系和世界类型
2 |
3 | 在这节中,我们将来学习如何添加一个自定义的生物群系,
4 |
5 | 和结构类似,首先我们的新建一个类,继承原版的生物群系。
6 |
7 | ```java
8 | public class ObsidianBiome extends Biome {
9 | protected ObsidianBiome(Builder biomeBuilder) {
10 | super(biomeBuilder);
11 | this.addFeature(GenerationStage.Decoration.UNDERGROUND_ORES, Feature.ORE.withConfiguration(new OreFeatureConfig(OreFeatureConfig.FillerBlockType.NATURAL_STONE, Blocks.GOLD_ORE.getDefaultState(), 9)).withPlacement(Placement.COUNT_RANGE.configure(new CountRangeConfig(20, 32, 32, 80))));
12 | this.addSpawn(EntityClassification.AMBIENT, new SpawnListEntry(EntityType.WITHER_SKELETON, 30, 5, 10));
13 | DefaultBiomeFeatures.addCarvers(this);
14 | }
15 | }
16 | ```
17 |
18 | 可以看到这里的内容非常的简单,不过就是给我的的生物群系添加`Feature`子类的,之前已经写过自然生成的大家对这些方法并不陌生。唯一一个值得说的是`DefaultBiomeFeatures`,原版在这里定义了一些预先设置好的`Feature`、`Structure`、`Carvers`和`Spawn`,大家可以直接调用里面自带的方法。
19 |
20 | 接下来就是注册我们的生物群系
21 |
22 | ```java
23 | public class BiomeRegistry {
24 | public static final DeferredRegister BIOMES = new DeferredRegister<>(ForgeRegistries.BIOMES, "neutrino");
25 | public static RegistryObject obsidianBiome = BIOMES.register("obsidian_biome", () -> {
26 | return new ObsidianBiome(new Biome.Builder().category(Biome.Category.PLAINS)
27 | .surfaceBuilder(SurfaceBuilder.DEFAULT,
28 | new SurfaceBuilderConfig(Blocks.OBSIDIAN.getDefaultState(), Blocks.STONE.getDefaultState(), Blocks.END_STONE.getDefaultState())
29 | )
30 | .scale(3f)
31 | .downfall(0.5f)
32 | .precipitation(Biome.RainType.SNOW)
33 | .depth(1f)
34 | .temperature(0.7f)
35 | .waterColor(0x0c0a15)
36 | .waterFogColor(0x632ebf)
37 | );
38 | });
39 | }
40 | ```
41 |
42 | 这里的内容也类似,因为这里的参数过多,很多参数也没发简单的解释,这里就不多加解释为什么了。我建议大家自行参照原版的实现填写内容。
43 |
44 | 当然别忘了在Mod主类中,将`BIOMES`注册到Mod总线中。
45 |
46 | 创建完成之后,接下来就是将我们的生物群系添加到主世界的世界生成中。
47 |
48 | ```java
49 | @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
50 | public class CommonSetupEvent {
51 | @SubscribeEvent
52 | public static void onCommonSetup(FMLCommonSetupEvent event) {
53 | BiomeManager.addBiome(BiomeManager.BiomeType.COOL, new BiomeManager.BiomeEntry(BiomeRegistry.obsidianBiome.get(), 1000));
54 | }
55 | }
56 | ```
57 |
58 | 这里我们在`FMLCommonSetupEvent`世界中,调用`BiomeManager.addBiome`添加了我们的生物群系,`BiomeManager.BiomeEntry`构造方法的第二个参数是权重,这里我们填的高点让我们的生物群系更容易被找到。
59 |
60 | 打开游戏寻找一下,应该就能看见我们的生物群系了。
61 |
62 | 
63 |
64 | 但是寻找生物群系有时候是非常麻烦的事情,我们可以创建一个`WorldType`来帮助我们调试生物群系。所谓的`WorldType`就是原版中的`默认`、`超平坦`、`巨大化`等。
65 |
66 | 当然,创建一个自定义的也非常简单
67 |
68 | ```java
69 | public class ObsidianWorldType extends WorldType {
70 | public ObsidianWorldType() {
71 | super("neutrino_type");
72 | }
73 |
74 | @Override
75 | public ChunkGenerator> createChunkGenerator(World world) {
76 | OverworldGenSettings settings = new OverworldGenSettings();
77 | SingleBiomeProvider singleBiomeProvider = new SingleBiomeProvider(new SingleBiomeProviderSettings(world.getWorldInfo()).setBiome(BiomeRegistry.obsidianBiome.get()));
78 | return new OverworldChunkGenerator(world, singleBiomeProvider, settings);
79 | }
80 | }
81 | ```
82 |
83 | 这里我们直接继承了`WorldType`类。
84 |
85 | 它的构造方法里需要填入一个名字,请注意这个名字不能超过16个字符
86 |
87 | 然后就是他的`createChunkGenerator`方法,这里需要返回一个`ChunkGenerator`,因为我们希望能像原版主世界的一样的地形起伏,所以返回的是`OverworldChunkGenerator`。因为我们只需要生成一种生物群系,所以在这里用了 `SingleBiomeProvider`(单一生物群系提供器)。还有一个`OverworldGenSettings`就没什么好讲的了。
88 |
89 | 创建完成之后,我们只需要在你的主类中,创建一个变量实例化它就行。
90 |
91 | ```java
92 | @Mod("neutrino")
93 | public class Neutrino {
94 | public static final ObsidianWorldType obsidianWorldType = new ObsidianWorldType();
95 | public Neutrino() {
96 | ...代码省略...
97 | ```
98 |
99 | 此时打开游戏,你应该就能看见新的世界类型了
100 |
101 | 
102 |
103 | 创建一个存档试试吧。
104 |
105 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/biome)
106 |
107 |
--------------------------------------------------------------------------------
/src/at/intro.md:
--------------------------------------------------------------------------------
1 | # Access Transformer
2 |
3 | **请注意,在没有确定你必须要使用AT的情况下,请务必不要随意使用AT,会有兼容性风险**
4 |
5 | 虽然Forge已经为我们做了绝大多数的工作来方便我们开发mod,但是在极少数的情况下,我们会发现Forge没有提供给我们接口让我来修改原版的内容,而原版的方法、字段等又是`private`的,我们没法调用它们。
6 |
7 | 在这种情况下,Forge提供给我了一种叫做`Access Transformer`(简称`AT`)的工具,让我们能够修改原版内容的调用权限,把`private`改成`public`。
8 |
9 | 这里我们以给堆肥桶添加新的物品为例。
10 |
11 | 如果你查看过原版`ComposterBlock`的代码,你会注意到,原版调用了`registerCompostable`函数来添加堆肥桶运行添加进入的物品,但是这个方法是`private`的,我们没法直接调用。
12 |
13 | ```java
14 | private static void registerCompostable(float chance, IItemProvider itemIn) {
15 | CHANCES.put(itemIn.asItem(), chance);
16 | }
17 | ```
18 |
19 | 这时我们就可以使用AT来把这个`private`变成`public`,来向堆肥桶里添加新的物品。
20 |
21 | 首先我们需要编辑我们的`build.gralde`中,如下这部分,将`AT`相关的注释删除。
22 |
23 | ```groovy
24 | minecraft {
25 | // The mappings can be changed at any time, and must be in the following format.
26 | // snapshot_YYYYMMDD Snapshot are built nightly.
27 | // stable_# Stables are built at the discretion of the MCP team.
28 | // Use non-default mappings at your own risk. they may not always work.
29 | // Simply re-run your setup task after changing the mappings to update your workspace.
30 | mappings channel: 'snapshot', version: '20200512-1.15.1'
31 | // makeObfSourceJar = false // an Srg named sources jar is made by default. uncomment this to disable.
32 |
33 | accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg')
34 | ```
35 |
36 | 然后在`META-INF`文件夹下,创建`accesstransformer.cfg`文件。
37 |
38 | ```
39 | .
40 | ├── META-INF
41 | │ ├── accesstransformer.cfg
42 | │ └── mods.toml
43 | ├── assets
44 | ├── data
45 | └── pack.mcmeta
46 | ```
47 |
48 | 在`accesstransformer.cfg`里填入的就是你的`AT`指令。
49 |
50 | 具体的格式如下,内容出自[Sponge 文档](https://docs.spongepowered.org/stable/zh-CN/plugin/internals/access-transformers.html)
51 |
52 | > 有三种不同类型的 AT,分别用于对类、字段、和方法作出修改。Access Transformer配置中的每一行由两个(对于类而言)或三个(对于方法和字段而言)部分,部分与部分之间使用空格隔开。
53 | >
54 | > - 用于修改方法或字段的访问级标识符,如 `public` 或 `protected` 等。如果你还想同时去除 `final` 修饰符,你可以在其后添加 `-f` 标记,如 `public-f`
55 | > - 一个类的全名,如 `net.minecraft.server.MinecraftServer`
56 | > - 字段或方法的 Searge 名(方法需要添加相应的方法签名),如 `field_54654_a` 或 `func_4444_a()V`
57 | >
58 | > 你可以通过添加 `#` 前缀以添加注释。一个良好的习惯是为每行 AT 都写相应的注释,这样每一个字段或方法的引用就一目了然。
59 | >
60 | > 下面是几行 Access Transformer 的示例:
61 | >
62 | > ```
63 | > public-f net.minecraft.server.MinecraftServer field_71308_o # anvilFile
64 | > public net.minecraft.server.MinecraftServer func_71260_j()V # stopServer
65 | > public-f net.minecraft.item.ItemStack
66 | > ```
67 |
68 | 如果你需要获取原版的SRG和MCP映射,你可以运行ForgeGradle提供的`creteMcpToSrg`task.
69 |
70 | 运行结束后,你就可以在`build/createSrgToMcp`目录下,找到对应的表了。
71 |
72 | 
73 |
74 | 但其实,我们不需要直接写`AT`,在discord 上有个叫做`K9`的机器,你可以在MCP Bot的[Discord Server](https://discord.gg/h4whGT9 )中的`#bot-spam`找到它,建议是直接私聊它。
75 |
76 | 输入`!mcp 你要查询的内容`,在我们的例子里就是`!mcp registerCompostable`,它会自动返回相关的SRG名等信息,其中就包括了AT。
77 |
78 | 
79 |
80 | 其中最下面的这一行就是AT。
81 |
82 | 然后我们将其复制到`accesstransformer.cfg`中:
83 |
84 | ```
85 | public net.minecraft.block.ComposterBlock func_220290_a(FLnet/minecraft/util/IItemProvider;)V # registerCompostable
86 | ```
87 |
88 | 接下来,我们需要刷新我们的Gradle。
89 |
90 | 
91 |
92 | 点击Gradle面板中的`Reload All Gradle Projects`,等待刷新完成。
93 |
94 | 在刷新完成后,你再次查看`ComposterBlock`的代码,你就会发现`registerCompostable`这个静态方法变成`public`的了。
95 |
96 | 接下来我们就可以来添加新的物品了。
97 |
98 | ```java
99 | @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
100 | public class ATDemo {
101 | @SubscribeEvent
102 | public static void addNewItemToComposterBlock(FMLLoadCompleteEvent event) {
103 | ComposterBlock.registerCompostable(0.3F, Items.OAK_LEAVES);
104 | ComposterBlock.registerCompostable(0.3F, Items.SPRUCE_LEAVES);
105 | ComposterBlock.registerCompostable(0.3F, Items.BIRCH_LEAVES);
106 | ComposterBlock.registerCompostable(0.3F, Items.JUNGLE_LEAVES);
107 | ComposterBlock.registerCompostable(0.3F, Items.ACACIA_LEAVES);
108 | ComposterBlock.registerCompostable(0.3F, Items.DARK_OAK_LEAVES);
109 | }
110 | }
111 | ```
112 |
113 | 这里我们将原版的一些树叶添加了进去,启动游戏,现在你应该就可以向堆肥器里塞入树叶了。
114 |
115 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/at)
--------------------------------------------------------------------------------
/src/entity/passiveentityandai.md:
--------------------------------------------------------------------------------
1 | # 创建一个动物和AI
2 |
3 | 虽然上节中,我们从零创建了一个最简单的实体,但是其实你在写mod的大部分时候都不需要直继承`Entity`类,原版已经写好了非常多的类供你使用。在这节中我们将创建一个黑曜石动物,并且为这个动物添加一个AI。
4 |
5 | 这里就引出的第一个问题,什么是AI?AI代表着生物的特殊的行为,比如牛会被玩家用小麦吸引,怪物会攻击玩家,会在村庄里走来走去。这些都是通过AI实现的,请注意不是所有的实体都可以有AI,AI是`MobEntity` 的子类特有的行为。
6 |
7 | 首先我们来创建我们的实体类,`ObsidianAnimal`:
8 |
9 | ```java
10 | public class ObsidianAnimal extends AnimalEntity {
11 | protected ObsidianAnimal(EntityType extends AnimalEntity> type, World worldIn) {
12 | super(type, worldIn);
13 | this.goalSelector.addGoal(0, new ObsidianGoal(this));
14 | }
15 |
16 | @Nullable
17 | @Override
18 | public AgeableEntity createChild(AgeableEntity ageable) {
19 | return null;
20 | }
21 | }
22 | ```
23 |
24 | 可以看见内容非常简单,甚至比我们第一个实体的内容还要简单。我们先来看:
25 |
26 | ```java
27 | @Nullable
28 | @Override
29 | public AgeableEntity createChild(AgeableEntity ageable) {
30 | return null;
31 | }
32 | ```
33 |
34 | 这个方法是用来创建后代用的,因为我们的动物并并没有后代,所以这里就返回空即可。
35 |
36 | 然后我们来看构造方法。
37 |
38 | ```java
39 | protected ObsidianAnimal(EntityType extends AnimalEntity> type, World worldIn) {
40 | super(type, worldIn);
41 | this.goalSelector.addGoal(0, new ObsidianGoal(this));
42 | }
43 | ```
44 |
45 | 在这里我们调用` this.goalSelector.addGoal`方法为我们的实体添加了一个AI或者说Goal(目的)。
46 |
47 | `ObsidianGoal`:
48 |
49 | ```java
50 | public class ObsidianGoal extends Goal {
51 | private ObsidianAnimal obsidianAnimal;
52 |
53 | public ObsidianGoal(ObsidianAnimal obsidianAnimal) {
54 | this.obsidianAnimal = obsidianAnimal;
55 | }
56 |
57 | @Override
58 | public boolean shouldExecute() {
59 | World world = this.obsidianAnimal.getEntityWorld();
60 | if (!world.isRemote) {
61 | BlockPos blockPos = this.obsidianAnimal.getPosition();
62 | PlayerEntity player = world.getClosestPlayer(blockPos.getX(), blockPos.getY(), blockPos.getZ(), 10, false);
63 | if (player != null) {
64 | player.addPotionEffect(new EffectInstance(Effects.HUNGER, 3 * 20, 3));
65 | }
66 | }
67 | return true;
68 | }
69 | }
70 | ```
71 |
72 | 在默认情况下`Goal`的对构造方法是没有什么要求的,但是在大多数时候,你都应该在构造方法,将实体的实例传递进来并保存,这样会方便你实现实体的AI。`shouldExecute`是`Goal`最为重要的方法,这里就是调用你实体AI的地方,这里我们的AI非常简单,就是给靠近实体的玩家一个饥饿效果,实现原理和我们之前实现的会播放僵尸吼叫声的方块原理是一样的,这里就不加赘述了。
73 |
74 | 然后是模型`ObsidianAnimalModel`:
75 |
76 | ```java
77 | public class ObsidianAnimalModel extends EntityModel {
78 | private final ModelRenderer body;
79 |
80 | public ObsidianAnimalModel() {
81 | textureWidth = 64;
82 | textureHeight = 64;
83 |
84 | body = new ModelRenderer(this);
85 | body.setRotationPoint(8.0F, 24.0F, -8.0F);
86 | body.setTextureOffset(0, 0).addBox(-16.0F, -16.0F, 0.0F, 16.0F, 10.0F, 16.0F, 0.0F, false);
87 | }
88 |
89 | @Override
90 | public void setRotationAngles(ObsidianAnimal entityIn, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch) {
91 | }
92 |
93 | @Override
94 | public void render(MatrixStack matrixStackIn, IVertexBuilder bufferIn, int packedLightIn, int packedOverlayIn, float red, float green, float blue, float alpha) {
95 | body.render(matrixStackIn, bufferIn, packedLightIn, packedOverlayIn);
96 | }
97 | }
98 | ```
99 |
100 | 可以看到我们的模型也就是一个普通的方块而已。
101 |
102 | 然后是渲染
103 |
104 | ```java
105 | public class ObsidianAnimalRender extends MobRenderer {
106 |
107 | public ObsidianAnimalRender(EntityRendererManager renderManagerIn) {
108 | super(renderManagerIn, new ObsidianAnimalModel(), 1F);
109 | }
110 |
111 | public ObsidianAnimalRender(EntityRendererManager renderManagerIn, ObsidianAnimalModel entityModelIn, float shadowSizeIn) {
112 | super(renderManagerIn, entityModelIn, shadowSizeIn);
113 | }
114 |
115 | @Override
116 | public ResourceLocation getEntityTexture(ObsidianAnimal entity) {
117 | return new ResourceLocation("neutrino", "textures/entity/obsidian_animal.png");
118 | }
119 | }
120 | ```
121 |
122 | 这里我们直接继承了`MobRenderer`来自动的渲染一些类似于影子的东西。之所以这里有两个构造函数是因为,当我们注册Render的时候,Lambda表达式里只给了一个参数,虽然你也可以把预设的内容写在Lambda表达式里,但是如果你那样干了就没法简化代码了,所以我们这里就额外添加了一个构造函数。
123 |
124 | 这里构造函数的第二个参数是你的动物的模型,第三个参数是影子的大小。
125 |
126 | 别忘了注册你的实体和你的Render。
127 |
128 | 组成完成后,输入命令召唤实体。
129 |
130 | 
131 |
132 | 可以看到,当你靠近实体时,你就获得了一个饥饿效果,试试用剑杀死它吧。
133 |
134 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/obsidian_animal)
135 |
136 |
--------------------------------------------------------------------------------
/src/tileentity/datasync.md:
--------------------------------------------------------------------------------
1 | # 方块实体内置的数据同步
2 |
3 | 在这节中,我们将学习方块实体中内置的数据同步功能。
4 |
5 | 还记得我们之前讲过的服务端和客户端自己的数据同步吗?幸运的是,TileEntity给我们内置了两组数据同步的方法,不幸的是这个两组方法只能实现从服务端到客户端的数据同步,而且只能同步少量的数据。
6 |
7 | 在这节中,我们讲学习制作一个每隔几秒中就会播放僵尸吼叫声的方块,我们先从方块开始;
8 |
9 | `ObsidianZombieBlock`
10 |
11 | ```java
12 | public class ObsidianZombieBlock extends Block {
13 | public ObsidianZombieBlock() {
14 | super(Properties.create(Material.ROCK).hardnessAndResistance(5));
15 | }
16 |
17 | @Override
18 | public boolean hasTileEntity(BlockState state) {
19 | return true;
20 | }
21 |
22 | @Nullable
23 | @Override
24 | public TileEntity createTileEntity(BlockState state, IBlockReader world) {
25 | return new ObsidianZombieTileEntity();
26 | }
27 | }
28 | ```
29 |
30 | 然后是`ObsidianZombieTileEntity`。
31 |
32 | ```java
33 | public class ObsidianZombieTileEntity extends TileEntity implements ITickableTileEntity {
34 | private boolean flag = false;
35 | private int MAX_TIME = 5 * 20;
36 | private int timer = 0;
37 |
38 |
39 | public ObsidianZombieTileEntity() {
40 | super(TileEntityTypeRegistry.obsidianZombieTileentity.get());
41 | }
42 |
43 | @Override
44 | public void tick() {
45 | if (world.isRemote && flag) {
46 | PlayerEntity player = world.getClosestPlayer(pos.getX(), pos.getY(), pos.getZ(), 10, false);
47 | this.world.playSound(player, pos, SoundEvents.ENTITY_ZOMBIE_AMBIENT, SoundCategory.AMBIENT, 1.0f, 1.0f);
48 | flag = false;
49 | }
50 | if (!world.isRemote) {
51 | if (timer == MAX_TIME) {
52 | flag = true;
53 | world.notifyBlockUpdate(pos, getBlockState(), getBlockState(), Constants.BlockFlags.BLOCK_UPDATE);
54 | flag = true;
55 | timer = 0;
56 | }
57 | timer++;
58 | }
59 | }
60 |
61 | @Nullable
62 | @Override
63 | public SUpdateTileEntityPacket getUpdatePacket() {
64 | return new SUpdateTileEntityPacket(pos, 1, getUpdateTag());
65 | }
66 |
67 | @Override
68 | public void onDataPacket(NetworkManager net, SUpdateTileEntityPacket pkt) {
69 | handleUpdateTag(pkt.getNbtCompound());
70 | }
71 |
72 | @Override
73 | public CompoundNBT getUpdateTag() {
74 | CompoundNBT compoundNBT = super.getUpdateTag();
75 | compoundNBT.putBoolean("flag", flag);
76 | return compoundNBT;
77 | }
78 |
79 | @Override
80 | public void handleUpdateTag(CompoundNBT tag) {
81 | flag = tag.getBoolean("flag");
82 | }
83 | }
84 | ```
85 |
86 | 首先我们来讲解两组同步数据用的方法。
87 |
88 | - `getUpdatePacket`
89 | - `onDataPacket`
90 |
91 | 这两个方法是在正常游戏过程中会被使用的数据同步方法,它们的名字可能会有些迷惑性,因为`getUpdatePacket`是服务端发送数据包用的方法,而`onDataPacket`才是客户端接受数据包的方法。
92 |
93 | 接下来是:
94 |
95 | - `getUpdateTag`
96 | - `handleUpdateTag`
97 |
98 | 这两个方法是在区块刚被载入时被调用的方法,之所以有这两个方法存在,是因为有些装饰性的方块实体并不需要经常性地同步数据,比如告示牌,只需要在区块被载入时同步一次就行。
99 |
100 | 为了让你的方块实体能在游戏被载入的时候能自动同步数据,你需要先实现`getUpdateTag`和`handleUpdateTag`,然后在`getUpdatePacket`和`onDataPacket`中调用它们即可。
101 |
102 | 如你你需要触发数据同步,你需要在服务端调用`world.notifyBlockUpdate`方法。
103 |
104 | 当然就和数据保存一样,这些方法默认的序列化和反序列化方式是NBT标签。
105 |
106 | ```java
107 | @Override
108 | public CompoundNBT getUpdateTag() {
109 | CompoundNBT compoundNBT = super.getUpdateTag();
110 | compoundNBT.putBoolean("flag", flag);
111 | return compoundNBT;
112 | }
113 |
114 | @Override
115 | public void handleUpdateTag(CompoundNBT tag) {
116 | flag = tag.getBoolean("flag");
117 | }
118 | ```
119 |
120 | 可以看到,用NBT标签传输了数据。
121 |
122 | ```java
123 | @Override
124 | public SUpdateTileEntityPacket getUpdatePacket() {
125 | return new SUpdateTileEntityPacket(pos, 1, getUpdateTag());
126 | }
127 |
128 | @Override
129 | public void onDataPacket(NetworkManager net, SUpdateTileEntityPacket pkt) {
130 | handleUpdateTag(pkt.getNbtCompound());
131 | }
132 | ```
133 |
134 | 在这里我们只是简单的调用了之前的两个方法而已,`SUpdateTileEntityPacket`的第二个参数的序列号值,大家可以随便填写。
135 |
136 | 然后就是我们的主要逻辑`tick`方法:
137 |
138 | ```java
139 | @Override
140 | public void tick() {
141 | if (world.isRemote && flag) {
142 | PlayerEntity player = world.getClosestPlayer(pos.getX(), pos.getY(), pos.getZ(), 10, false);
143 | this.world.playSound(player, pos, SoundEvents.ENTITY_ZOMBIE_AMBIENT, SoundCategory.AMBIENT, 1.0f, 1.0f);
144 | flag = false;
145 | }
146 | if (!world.isRemote) {
147 | if (timer == MAX_TIME) {
148 | flag = true;
149 | world.notifyBlockUpdate(pos, getBlockState(), getBlockState(), Constants.BlockFlags.BLOCK_UPDATE);
150 | flag = true;
151 | timer = 0;
152 | }
153 | timer++;
154 | }
155 | }
156 | ```
157 |
158 | 为了大家方便理解,我这里用了两个`if`语句将客户端和服务端的逻辑区分开来
159 |
160 | 首先是是客户端:
161 |
162 | ```java
163 | if (world.isRemote && flag) {
164 | PlayerEntity player = world.getClosestPlayer(pos.getX(), pos.getY(), pos.getZ(), 10, false);
165 | this.world.playSound(player, pos, SoundEvents.ENTITY_ZOMBIE_AMBIENT, SoundCategory.AMBIENT, 1.0f, 1.0f);
166 | flag = false;
167 | }
168 | ```
169 |
170 | 首先我们判断是否在客户端,并且flag的值是否为真(为真代表要播放声音),然后获取了最近的玩家,使用了`this.world.playSound`方法播放了声音,请注意,播放声音可以同时在客户端和服务端执行,如果你在服务端执行会自动发一个数据包到客户端,让客户端播放声音。
171 |
172 | `playSound`的第一个和第二个变量很好懂,第三个变量是需要播放的声音,第三个变量是决定这个声音大小是受到哪一个声音控制分类控制的。
173 |
174 | 接下来是服务端:
175 |
176 | ```java
177 | if (!world.isRemote) {
178 | if (timer == MAX_TIME) {
179 | flag = true;
180 | world.notifyBlockUpdate(pos, getBlockState(), getBlockState(), Constants.BlockFlags.BLOCK_UPDATE);
181 | flag = true;
182 | timer = 0;
183 | }
184 | timer++;
185 | }
186 | ```
187 |
188 | 服务端和之前一样基本上就是一个计数器,唯一特殊的地方就是我们调用了` world.notifyBlockUpdate`方法,因为我们不需要更新`blockstate`,所以第二和第三个参数保持相同就行,最后一个参数是需要什么等级的更新,Forge提供给我们了`Constants`类,里面有详细的说明。
189 |
190 | 然后就是添加材质和模型之类的事了,有兴趣的读者可以自己看源代码。
191 |
192 | 打开游戏放置完方块,等待一会你应该就能听见僵尸的吼叫声了。
193 |
194 | 
195 |
196 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/tileentitydatasync)
197 |
198 |
--------------------------------------------------------------------------------
/src/block/nonesoildblock.md:
--------------------------------------------------------------------------------
1 | # 非实心方块与自定义模型
2 |
3 | 在这一节中,我们将创建一个有着特殊外形且是透明的方块,这里我们以黑曜石框架举例。
4 |
5 | 首先我们创建一个叫做`ObsidianFrame`的类,内容如下:
6 |
7 | ```java
8 | public class ObsidianFrame extends Block {
9 | private static VoxelShape shape;
10 |
11 | static {
12 | VoxelShape base = Block.makeCuboidShape(0, 0, 0, 16, 1, 16);
13 | VoxelShape column1 = Block.makeCuboidShape(0, 1, 0, 1, 15, 1);
14 | VoxelShape column2 = Block.makeCuboidShape(15, 1, 0, 16, 15, 1);
15 | VoxelShape column3 = Block.makeCuboidShape(0, 1, 15, 1, 15, 16);
16 | VoxelShape column4 = Block.makeCuboidShape(15, 1, 15, 16, 15, 16);
17 | VoxelShape top = Block.makeCuboidShape(0, 15, 0, 16, 16, 16);
18 | shape = VoxelShapes.or(base, column1, column2, column3, column4, top);
19 | }
20 |
21 | public ObsidianFrame() {
22 | super(Properties.create(Material.ROCK).hardnessAndResistance(5).notSolid());
23 | }
24 |
25 | @Override
26 | public VoxelShape getShape(BlockState state, IBlockReader worldIn, BlockPos pos, ISelectionContext context) {
27 | return shape;
28 | }
29 | }
30 | ```
31 |
32 | 在最上方我们创建了一个`VoxelShape`,我们将在`getShape`方法中返回这个形状,这个``VoxelShape``就是我们方块的碰撞箱,很不幸的是Minecraft的碰撞箱子只能由方块组成,这个方块的大小是`16*16*16`,所以我们在静态代码块中自己创建了一系列的长方体和立方体,拼成了我们方块的碰撞箱,其中`Block.makeCuboidShape`的6个参数分别是起始点的XYZ和结束点的XYZ。最后我们用`VoxelShapes`的`or`方法将这些东西拼在了一起。`VoxelShapes` 下还有很多好用的空间操作方法,请自行选用。如果你不给你的方块设置合适的的碰撞箱的话,你的方块内部空间会显得非常的暗。
33 |
34 | 可以看见这里最为特别的是调用了`notSoild`方法,这个方法是告知Minecraft我们的方块不是一个「实心」方块,需要进行特殊的对待。之所以这么做,是因为Minecraft的世界里有非常多的方块,如果方块的每一个面都要渲染,包括那些被遮挡的面和遮挡起来的方块,那么会非常地耗费性能,所以出于优化的考虑,Minecraft只会渲染那些没有被遮挡起来的面。而`noSoild`的作用就是告诉Minecraft,要渲染这个方块遮挡的那些面。
35 |
36 | 如果不开启这个就会出现这样效果。
37 |
38 | 
39 |
40 | 如果对Minecraft方块渲染相关的内容感兴趣,可以阅读这篇博客[文章](https://greyminecraftcoder.blogspot.com/2020/04/block-rendering-1144.html),以及这个博客下其他文章(如果你打不开这个页面,说明你所在的国家或地区封锁了这个网站)。
41 |
42 | 注册方块:
43 |
44 | ```java
45 | public static RegistryObject obsidianFrame = BLOCKS.register("obsidian_frame", () -> {
46 | return new ObsidianFrame();
47 | });
48 | ```
49 |
50 | 注册物品:
51 |
52 | ```java
53 | public static RegistryObject- obssidianFrame = ITEMS.register("obsidian_frame", () -> {
54 | return new BlockItem(BlockRegistry.obsidianFrame.get(), new Item.Properties().group(ModGroup.itemGroup));
55 | });
56 | ```
57 |
58 |
59 |
60 | 然后是方块状态文件`obsidian_frame.json`:
61 |
62 | ```json
63 | {
64 | "variants": {
65 | "": { "model": "neutrino:block/obsidian_frame" }
66 | }
67 | }
68 | ```
69 |
70 | 模型文件`obsidian_frame.json`:
71 |
72 | ```json
73 | {
74 | "credit": "Made with Blockbench",
75 | "texture_size": [64, 64],
76 | "textures": {
77 | "0": "neutrino:block/obsidian_frame",
78 | "particle": "neutrino:block/obsidian_frame"
79 | },
80 | "elements": [
81 | {
82 | "from": [0, 0, 0],
83 | "to": [16, 1, 16],
84 | "faces": {
85 | "north": {"uv": [4, 8.25, 8, 8.5], "texture": "#0"},
86 | "east": {"uv": [0, 8.25, 4, 8.5], "texture": "#0"},
87 | "south": {"uv": [12, 8.25, 16, 8.5], "texture": "#0"},
88 | "west": {"uv": [8, 8.25, 12, 8.5], "texture": "#0"},
89 | "up": {"uv": [8, 8.25, 4, 4.25], "texture": "#0"},
90 | "down": {"uv": [12, 4.25, 8, 8.25], "texture": "#0"}
91 | }
92 | },
93 | {
94 | "from": [0, 15, 0],
95 | "to": [16, 16, 16],
96 | "faces": {
97 | "north": {"uv": [4, 4, 8, 4.25], "texture": "#0"},
98 | "east": {"uv": [0, 4, 4, 4.25], "texture": "#0"},
99 | "south": {"uv": [12, 4, 16, 4.25], "texture": "#0"},
100 | "west": {"uv": [8, 4, 12, 4.25], "texture": "#0"},
101 | "up": {"uv": [8, 4, 4, 0], "texture": "#0"},
102 | "down": {"uv": [12, 0, 8, 4], "texture": "#0"}
103 | }
104 | },
105 | {
106 | "from": [0, 1, 0],
107 | "to": [1, 15, 1],
108 | "faces": {
109 | "north": {"uv": [2.25, 0.25, 2.5, 3.75], "texture": "#0"},
110 | "east": {"uv": [2, 0.25, 2.25, 3.75], "texture": "#0"},
111 | "south": {"uv": [2.75, 0.25, 3, 3.75], "texture": "#0"},
112 | "west": {"uv": [2.5, 0.25, 2.75, 3.75], "texture": "#0"},
113 | "up": {"uv": [2.5, 0.25, 2.25, 0], "texture": "#0"},
114 | "down": {"uv": [2.75, 0, 2.5, 0.25], "texture": "#0"}
115 | }
116 | },
117 | {
118 | "from": [15, 1, 0],
119 | "to": [16, 15, 1],
120 | "faces": {
121 | "north": {"uv": [1.25, 0.25, 1.5, 3.75], "texture": "#0"},
122 | "east": {"uv": [1, 0.25, 1.25, 3.75], "texture": "#0"},
123 | "south": {"uv": [1.75, 0.25, 2, 3.75], "texture": "#0"},
124 | "west": {"uv": [1.5, 0.25, 1.75, 3.75], "texture": "#0"},
125 | "up": {"uv": [1.5, 0.25, 1.25, 0], "texture": "#0"},
126 | "down": {"uv": [1.75, 0, 1.5, 0.25], "texture": "#0"}
127 | }
128 | },
129 | {
130 | "from": [0, 1, 15],
131 | "to": [1, 15, 16],
132 | "faces": {
133 | "north": {"uv": [0.25, 0.25, 0.5, 3.75], "texture": "#0"},
134 | "east": {"uv": [0, 0.25, 0.25, 3.75], "texture": "#0"},
135 | "south": {"uv": [0.75, 0.25, 1, 3.75], "texture": "#0"},
136 | "west": {"uv": [0.5, 0.25, 0.75, 3.75], "texture": "#0"},
137 | "up": {"uv": [0.5, 0.25, 0.25, 0], "texture": "#0"},
138 | "down": {"uv": [0.75, 0, 0.5, 0.25], "texture": "#0"}
139 | }
140 | },
141 | {
142 | "from": [15, 1, 15],
143 | "to": [16, 15, 16],
144 | "faces": {
145 | "north": {"uv": [3.25, 0.25, 3.5, 3.75], "texture": "#0"},
146 | "east": {"uv": [3, 0.25, 3.25, 3.75], "texture": "#0"},
147 | "south": {"uv": [3.75, 0.25, 4, 3.75], "texture": "#0"},
148 | "west": {"uv": [3.5, 0.25, 3.75, 3.75], "texture": "#0"},
149 | "up": {"uv": [3.5, 0.25, 3.25, 0], "texture": "#0"},
150 | "down": {"uv": [3.75, 0, 3.5, 0.25], "texture": "#0"}
151 | }
152 | }
153 | ]
154 | }
155 | ```
156 |
157 | 材质文件`obsidian_frame.png`:
158 |
159 |
160 |
161 | 这里我的模型和材质都是用BlockBench制作的。
162 |
163 | 物品模型`obsidian_frame.json`:
164 |
165 | ```json
166 | {
167 | "parent": "neutrino:block/obsidian_frame"
168 | }
169 | ```
170 |
171 | 打开游戏,你应该就能看见我们的黑曜石框架了。
172 |
173 | 
174 |
175 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/nonesoildblock)
176 |
177 |
--------------------------------------------------------------------------------
/src/fluid/firstfluid.md:
--------------------------------------------------------------------------------
1 | # 流体
2 |
3 | 在这节我们将来学习如何创建一个简单的流体。
4 |
5 | 首先我们要明确流体究竟是什么。你在游戏中接触的流体不外乎于两种形式,一种是装在「桶」里,另一个是在世界里不断流动。
6 |
7 | 当流体装在桶中,它属于一个叫做`BucketItem。`它流体在世界中不断流动,它属于的一种特殊的方块,这个方块和流体关联。(就像是方块和TileEntity关联)。
8 |
9 | 
10 |
11 | 这是一张调整过渲染模型的流体在世界中存在的情况,从这张图里,你应该能很明显的感觉到在世界中的流体也是以方块的形式存在的。
12 |
13 | 不过比起方块需要自己手动实现,Minecraft提供了一个叫做`FlowingFluidBlock`类,我们可以直接使用。
14 |
15 | 当然我还需要流体,流体有两种状态,一种是「Source(源)」,一种是「Flowing(流动)」,想想水源的流动的水,你应该就能区分它们了。Forge提供给我们了一个叫做`ForgeFlowingFluid`继承`FlowingFluid`,来方面我们创建流体。
16 |
17 | 接下来的内容可能会互相缠绕。我大家需要仔细梳理。
18 |
19 | 首先我们来看流体的注册。
20 |
21 | ```java
22 | public class FluidRegistry {
23 | public static final ResourceLocation STILL_OIL_TEXTURE = new ResourceLocation("block/water_still");
24 | public static final ResourceLocation FLOWING_OIL_TEXTURE = new ResourceLocation("block/water_flow");
25 |
26 | public static final DeferredRegister FLUIDS = new DeferredRegister<>(ForgeRegistries.FLUIDS, "neutrino");
27 | public static RegistryObject obsidianFluid = FLUIDS.register("obsidian_fluid", () -> {
28 | return new ForgeFlowingFluid.Source(FluidRegistry.PROPERTIES);
29 | });
30 | public static RegistryObject obsidianFluidFlowing = FLUIDS.register("obsidian_fluid_flowing", () -> {
31 | return new ForgeFlowingFluid.Flowing(FluidRegistry.PROPERTIES);
32 | });
33 | public static ForgeFlowingFluid.Properties PROPERTIES = new ForgeFlowingFluid.Properties(obsidianFluid, obsidianFluidFlowing, FluidAttributes.builder(STILL_OIL_TEXTURE, FLOWING_OIL_TEXTURE).color(0xFF311cbb).density(10)).bucket(ItemRegistry.obsidianFluidBucket).block(BlockRegistry.obsidianRubikCube).slopeFindDistance(3).explosionResistance(100F);
34 | }
35 | ```
36 |
37 | 可以看到,我们同样是使用了`DeferredRegister`来注册,这里只不过这里的泛型变成了`Fluid`。对于每一个流体你都需要做分别注册`Source`和`Flowing`。你可以看到我们分别调用了`ForgeFlowingFluid.Source`和`ForgeFlowingFluid.Flowing`来注册了这两部分。
38 |
39 | 注册流体时你需要传入一个`ForgeFlowingFluid.Properties`,这里属性规定了,`Source`和`Flowing`作为一个整体时的属性,比如当水源没有之后水流消失的速度,相对应的桶是什么,对应的方块是什么等。`bucket(ItemRegistry.obsidianFluidBucket)`这里我们设置的桶,`block(BlockRegistry.obsidianRubikCube)`这里我们设置了相对应的方块,接下去的两个是水流消失速度和防爆等级。
40 |
41 | 这个函数的前两个参数就是我们注册的`Source`和`Flowing`相对应的材质,这里我们就直接复用了原版水流的材质(请注意流体的材质需要是「[动态材质](https://minecraft-zh.gamepedia.com/index.php?title=资源包)」才能表现出流动的效果)。最后一个参数是一个`FluidAttributes`,这个是规定了这个流体的一些固有属性,比如颜色,稠度,温度,这里我们调用了`FluidAttributes.builder`来创建,调用`density(10)`设置了流体的稠度。
42 |
43 | 在这里我们还调用了`color(0xFF311cbb)`方法来设置流体的颜色,请注意这里的颜色是在你的贴图基础上叠加的颜色,这里颜色的顺序有点特别,每两位16进制数代表了ARGB的一个分量,顺序分别是alpha, red, green, blue,具体的信息请看`FluidAttributes` 类中`color`字段的相关注释。
44 |
45 | 当然流体还有很多的属性,具体的属性也请看`FluidAttributes`类中的注释。
46 |
47 | 在大部分时候流体都应该是半透明的,所以我们需要手动的设置流体的RenderType,这里别忘了`value = Dist.CLIENT`。
48 |
49 | ```java
50 | @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD,value = Dist.CLIENT)
51 | public class RenderTypeRegistry {
52 | @SubscribeEvent
53 | public static void onRenderTypeSetup(FMLClientSetupEvent event) {
54 | RenderTypeLookup.setRenderLayer(FluidRegistry.obsidianFluid.get(), RenderType.getTranslucent());
55 | RenderTypeLookup.setRenderLayer(FluidRegistry.obsidianFluidFlowing.get(), RenderType.getTranslucent());
56 | }
57 | }
58 | ```
59 |
60 | 当然别忘了将`FLUIDS`添加进注册总线中。
61 |
62 | 接下来是流体方块的注册。
63 |
64 | ```java
65 | public static RegistryObject obsidianRubikCube = BLOCKS.register("obsidian_fluid", () -> {
66 | return new FlowingFluidBlock(FluidRegistry.obsidianFluid, Block.Properties.create(Material.WATER).doesNotBlockMovement().hardnessAndResistance(100.0F).noDrops());
67 | });
68 | ```
69 |
70 | 可以看见,我们直接新建了一个`FlowingFluidBlock`的实例,并将直接注册好的流体传了进去。后面的方块属性读者应该看的懂,这里就不多加解释了。
71 |
72 | 然后是相对应桶的注册。
73 |
74 | ```java
75 | public static RegistryObject- obsidianFluidBucket = ITEMS.register("obsidian_fluid_bucket", () -> {
76 | return new BucketItem(FluidRegistry.obsidianFluid, new Item.Properties().group(ModGroup.itemGroup).containerItem(BUCKET));
77 | });
78 | ```
79 |
80 | 和方块类似这里就不多加说明了。
81 |
82 | 在流体的桶注册完成后,我还需要给桶设置在发射器发射时,能够放置我们的水源。
83 |
84 | ```java
85 | @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
86 | public class DispenserRegister {
87 | @SubscribeEvent
88 | public static void onDispenserRegister(FMLCommonSetupEvent event) {
89 | DispenserBlock.registerDispenseBehavior(ItemRegistry.obsidianFluidBucket.get(), new DefaultDispenseItemBehavior() {
90 | private final DefaultDispenseItemBehavior dispenseItemBehavior = new DefaultDispenseItemBehavior();
91 |
92 | /**
93 | * Dispense the specified stack, play the dispense sound and spawn particles.
94 | */
95 | public ItemStack dispenseStack(IBlockSource source, ItemStack stack) {
96 | BucketItem bucketitem = (BucketItem) stack.getItem();
97 | BlockPos blockpos = source.getBlockPos().offset(source.getBlockState().get(DispenserBlock.FACING));
98 | World world = source.getWorld();
99 | if (bucketitem.tryPlaceContainedLiquid(null, world, blockpos, null)) {
100 | bucketitem.onLiquidPlaced(world, stack, blockpos);
101 | return new ItemStack(Items.BUCKET);
102 | } else {
103 | return this.dispenseItemBehavior.dispense(source, stack);
104 | }
105 | }
106 | });
107 | }
108 | }
109 | ```
110 |
111 | 这里是照抄了原版的内容,具体的添加方式可以看`IDispenseItemBehavior`原版是怎么添加这个行为的。
112 |
113 | 当然别忘了给物品添加材质,大家可以按照原版水桶的材质进行修改。
114 |
115 | 打开游戏,你就能看到流体出现了。
116 |
117 | 
118 |
119 | 但是这时的流体还不能推动实体,你必须为你之前注册的流体添加原版`water`的[Tag(标签)](https://minecraft-zh.gamepedia.com/index.php?title=标签&variant=zh)才能推动实体。
120 |
121 | 在你的`resources`目录下按如下结构创建。
122 |
123 | ```
124 | resources
125 | ├── META-INF
126 | │ └── mods.toml
127 | ├── assets
128 | ├── data
129 | │ └── minecraft
130 | │ └── tags
131 | │ └── fluids
132 | └── pack.mcmeta
133 | ```
134 |
135 | 然后在`fluids`文件夹下创建`water.json`,内容如下:
136 |
137 | ```json
138 | {
139 | "replace": false,
140 | "values": [
141 | "neutrino:obsidian_fluid",
142 | "neutrino:obsidian_fluid_flowing"
143 | ]
144 | }
145 | ```
146 |
147 | 这里的值就是你的流体的两个注册名。
148 |
149 | 然后重新启动游戏,你的流体应该就能推动实体了。
--------------------------------------------------------------------------------
/src/specialrender/ter.md:
--------------------------------------------------------------------------------
1 | # TileEntityRenderer(方块实体渲染器)
2 |
3 | 在这节中,我们将来学习一个非常重要的渲染方式`TileEntityRenderer`简称`TER`。在开始之前我们首先需要知道`TER`的作用是什么。让我回想一下原版中的箱子、附魔台。它们都有特殊的动画,而`TER`的作用就是让你也可以实现这些动画。
4 |
5 | 首先我们从方块开始
6 |
7 | ```java
8 | public class ObsidianTERBlock extends Block {
9 |
10 | private static VoxelShape shape;
11 |
12 | static {
13 | VoxelShape base = Block.makeCuboidShape(0, 0, 0, 16, 1, 16);
14 | VoxelShape column1 = Block.makeCuboidShape(0, 1, 0, 1, 15, 1);
15 | VoxelShape column2 = Block.makeCuboidShape(15, 1, 0, 16, 15, 1);
16 | VoxelShape column3 = Block.makeCuboidShape(0, 1, 15, 1, 15, 16);
17 | VoxelShape column4 = Block.makeCuboidShape(15, 1, 15, 16, 15, 16);
18 | VoxelShape top = Block.makeCuboidShape(0, 15, 0, 16, 16, 16);
19 | shape = VoxelShapes.or(base, column1, column2, column3, column4, top);
20 | }
21 |
22 | public ObsidianTERBlock() {
23 | super(Properties.create(Material.ROCK).notSolid().hardnessAndResistance(5));
24 | }
25 |
26 | @Override
27 | public VoxelShape getShape(BlockState state, IBlockReader worldIn, BlockPos pos, ISelectionContext context) {
28 | return shape;
29 | }
30 |
31 | @Override
32 | public boolean hasTileEntity(BlockState state) {
33 | return true;
34 | }
35 |
36 | @Nullable
37 | @Override
38 | public TileEntity createTileEntity(BlockState state, IBlockReader world) {
39 | return new ObsidianTERTileEntity();
40 | }
41 |
42 | }
43 | ```
44 |
45 | 方块的内容非常简单,基本上就是抄了我们之前写好的黑曜石框架,然后给它添加了一个方块实体。
46 |
47 | 接下来我们来看方块实体的具体内容。
48 |
49 | ```java
50 | public class ObsidianTERTileEntity extends TileEntity {
51 | public ObsidianTERTileEntity() {
52 | super(TileEntityTypeRegistry.obsidianTERTileEntity.get());
53 | }
54 | }
55 | ```
56 |
57 | 因为我们用不到任何东西,所以这个方块实体是空的,仅仅用来和我们之后创建的`TileEntityRenderer`作关联。
58 |
59 | 接下来就是最为关键的`TER`部分了
60 |
61 | ```java
62 | public class ObsidianTER extends TileEntityRenderer {
63 |
64 | public ObsidianTER(TileEntityRendererDispatcher rendererDispatcherIn) {
65 | super(rendererDispatcherIn);
66 | }
67 |
68 | @Override
69 | public void render(ObsidianTERTileEntity tileEntityIn, float partialTicks, MatrixStack matrixStackIn, IRenderTypeBuffer bufferIn, int combinedLightIn, int combinedOverlayIn) {
70 | matrixStackIn.push();
71 | matrixStackIn.translate(1, 0, 0);
72 | BlockRendererDispatcher blockRenderer = Minecraft.getInstance().getBlockRendererDispatcher();
73 | BlockState state = Blocks.CHEST.getDefaultState();
74 | blockRenderer.renderBlock(state, matrixStackIn, bufferIn, combinedLightIn, combinedOverlayIn, EmptyModelData.INSTANCE);
75 | matrixStackIn.pop();
76 |
77 | matrixStackIn.push();
78 | matrixStackIn.translate(0, 1, 0);
79 | ItemRenderer itemRenderer = Minecraft.getInstance().getItemRenderer();
80 | ItemStack stack = new ItemStack(Items.DIAMOND);
81 | IBakedModel ibakedmodel = itemRenderer.getItemModelWithOverrides(stack, tileEntityIn.getWorld(), null);
82 | itemRenderer.renderItem(stack, ItemCameraTransforms.TransformType.FIXED, true, matrixStackIn, bufferIn, combinedLightIn, combinedOverlayIn, ibakedmodel);
83 | matrixStackIn.pop();
84 | }
85 | }
86 | ```
87 |
88 | 可以看到我们的`ObsidianTER`直接继承了`TileEntityRenderer`,其中泛型填的是你之前创建好的方块实体,
89 |
90 | 而其中的`render`方法,就是你的渲染方法。
91 |
92 | 因为这一块涉及到很多的计算机图形学相关的知识,这里我就不演示如何自定义渲染了,有兴趣的读者可以查看Mcjty做的相关的[教程](https://www.bilibili.com/video/BV1QE41137P9?p=14)和[代码](https://github.com/McJty/YouTubeModding14/blob/master/src/main/java/com/mcjty/mytutorial/blocks/MagicRenderer.java)。
93 |
94 | 这里我们只演示如何额外的渲染出物品和方块。
95 |
96 | 首先你应该用` matrixStackIn.push()`和`matrixStackIn.pop()`包裹起你的渲染代码。因为你对渲染做的所有移动、旋转和放大操作都会储存在`matrixStackIn`中,而`push`和`pop`的作用就是保存和恢复之前渲染的状态。如果你不进行这些操作有可能会污染其他的渲染器。
97 |
98 | ```java
99 | matrixStackIn.translate(1, 0, 0);
100 | ```
101 |
102 | 这个设置了你要渲染对象的移动
103 |
104 | 与此相对应的还有
105 |
106 | ```java
107 | matrixStackIn.rotate();
108 | matrixStackIn.scale();
109 | ```
110 |
111 | 分别用于旋转和缩放。
112 |
113 | ```java
114 | BlockRendererDispatcher blockRenderer = Minecraft.getInstance().getBlockRendererDispatcher();
115 | ItemRenderer itemRenderer = Minecraft.getInstance().getItemRenderer();
116 | ```
117 |
118 | 这两句都是用来获取物品和方块的渲染器。
119 |
120 | ```java
121 | BlockState state = Blocks.CHEST.getDefaultState();
122 | ItemStack stack = new ItemStack(Items.DIAMOND);
123 | ```
124 |
125 | 这两个变量的作用是用来指定你要渲染的方块和物品,我们这里将要渲染箱子的方块模型和钻石的物品模型。
126 |
127 | ```java
128 | IBakedModel ibakedmodel = itemRenderer.getItemModelWithOverrides(stack, tileEntityIn.getWorld(), null);
129 | ```
130 |
131 | 这句的作用是获取能物品的`IBakedModel`,之后用于渲染。
132 |
133 | ```java
134 | blockRenderer.renderBlock(state, matrixStackIn, bufferIn, combinedLightIn, combinedOverlayIn, EmptyModelData.INSTANCE);
135 | itemRenderer.renderItem(stack, ItemCameraTransforms.TransformType.FIXED, true, matrixStackIn, bufferIn, combinedLightIn, combinedOverlayIn, ibakedmodel);
136 | ```
137 |
138 | 在这里就是渲染指令,我们可以通过这两句话调用物品和方块默认的渲染方法。具体怎么渲染我们不用关心。
139 |
140 | 然后我们需要绑定我们的`TER`
141 |
142 | ```java
143 | @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
144 | public class ClientEventHandler {
145 | @SubscribeEvent
146 | public static void onClientEvent(FMLClientSetupEvent event) {
147 | ClientRegistry.bindTileEntityRenderer(TileEntityTypeRegistry.obsidianTERTileEntity.get(), (tileEntityRendererDispatcher -> {
148 | return new ObsidianTER(tileEntityRendererDispatcher);
149 | }));
150 | }
151 | }
152 | ```
153 |
154 | 可以看见我们在Mod总线中的`FMLClientSetupEvent`(客户端配置)事件中,调用`ClientRegistry.bindTileEntityRenderer`方法把我们的`TER`绑定到我们的方块实体上。
155 |
156 | 当然别忘了注册你的方块实体
157 |
158 | ```java
159 | public static RegistryObject> obsidianTERTileEntity = TILE_ENTITY_TYPE_DEFERRED_REGISTER.register("obsidian_ter_tileentity", () -> {
160 | return TileEntityType.Builder.create(() -> {
161 | return new ObsidianTERTileEntity();
162 | }, BlockRegistry.obsidianTERBlock.get()).build(null);
163 | });
164 | ```
165 |
166 | 打开游戏放下方块,你应该就能看见一个特殊的方块了。
167 |
168 | 
169 |
170 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/ter)
171 |
172 |
--------------------------------------------------------------------------------
/src/specialrender/ister.md:
--------------------------------------------------------------------------------
1 | # ItemStackTileEntityRenderer(物品特殊渲染)
2 |
3 | 这一节中,我们将要来学习Item的特殊渲染:`ItemStackTileEntityRenderer`简称`ISTER`,它的作用和`TileEntityRenderer`非常类似,在以前`ItemStackTileEntityRenderer`甚至就是靠`TileEntityRenderer`实现的。
4 |
5 | 利用`ISTER`你可以实现一些非常酷的渲染效果,举例来说Create(机械动力)中的扳手,它会自动旋转的齿轮就是利用`ISTER`实现的。
6 |
7 | 首先我们先来看物品的代码
8 |
9 | ```java
10 | public class ObsidianWrench extends Item {
11 | public ObsidianWrench() {
12 | super(new Properties().group(ModGroup.itemGroup).setISTER(() -> {
13 | return () -> {
14 | return new ObsidianWrenchItemStackTileEntityRenderer();
15 | };
16 | }));
17 | }
18 | }
19 | ```
20 |
21 | 可以看到物品的代码其实很简单,这里唯一特别的地方就是我们调用了`setISTER`方法,为我们的物品绑定了一个`setISTER`。但是这样绑定还不能直接用,我还得替换物品原本的`IBakedModel`,并在其中启用`ISTER`。
22 |
23 | 接下来我们来看替换物品`IBakedModel`的代码
24 |
25 | ```java
26 | @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
27 | public class ModBusEventHandler {
28 | @SubscribeEvent
29 | public static void onModelBaked(ModelBakeEvent event) {
30 | Map modelRegistry = event.getModelRegistry();
31 | ModelResourceLocation location = new ModelResourceLocation(ItemRegistry.obsidianWrench.get().getRegistryName(), "inventory");
32 | IBakedModel existingModel = modelRegistry.get(location);
33 | if (existingModel == null) {
34 | throw new RuntimeException("Did not find Obsidian Hidden in registry");
35 | } else if (existingModel instanceof ObsidianWrenchBakedModel) {
36 | throw new RuntimeException("Tried to replaceObsidian Hidden twice");
37 | } else {
38 | ObsidianWrenchBakedModel obsidianWrenchBakedModel = new ObsidianWrenchBakedModel(existingModel);
39 | event.getModelRegistry().put(location, obsidianWrenchBakedModel);
40 | }
41 | }
42 | }
43 | ```
44 |
45 | 可以看到,我们同样监听了`ModelBakeEvent`,然后通过`modelRegistry.get`获取了默认的`IBakedModel`并将它传入我们新的`IBakedModel`中,然后调用`event.getModelRegistry().put`替换了原版的`IBakedModel`,这里也别忘了`value = Dist.CLIENT`。
46 |
47 | 接下来看我们的`IBakedModel`:
48 |
49 | ```java
50 | public class ObsidianWrenchBakedModel implements IBakedModel {
51 | private IBakedModel existingModel;
52 |
53 | public ObsidianWrenchBakedModel(IBakedModel existingModel) {
54 | this.existingModel = existingModel;
55 | }
56 |
57 | @Nonnull
58 | @Override
59 | public List getQuads(@Nullable BlockState state, @Nullable Direction side, @Nonnull Random rand, @Nonnull IModelData extraData) {
60 | throw new AssertionError("IForgeBakedModel::getQuads should never be called, only IForgeBakedModel::getQuads");
61 | }
62 |
63 | @Override
64 | public List getQuads(@Nullable BlockState state, @Nullable Direction side, Random rand) {
65 | return this.existingModel.getQuads(state, side, rand);
66 | }
67 |
68 | @Override
69 | public boolean isAmbientOcclusion() {
70 | return this.existingModel.isAmbientOcclusion();
71 | }
72 |
73 | @Override
74 | public boolean isGui3d() {
75 | return this.existingModel.isGui3d();
76 | }
77 |
78 | @Override
79 | public boolean func_230044_c_() {
80 | return this.existingModel.func_230044_c_();
81 | }
82 |
83 | @Override
84 | public boolean isBuiltInRenderer() {
85 | return true;
86 | }
87 |
88 | @Override
89 | public TextureAtlasSprite getParticleTexture() {
90 | return this.existingModel.getParticleTexture();
91 | }
92 |
93 | @Override
94 | public ItemOverrideList getOverrides() {
95 | return this.existingModel.getOverrides();
96 | }
97 |
98 | @Override
99 | public IBakedModel handlePerspective(ItemCameraTransforms.TransformType cameraTransformType, MatrixStack mat) {
100 | if (cameraTransformType == ItemCameraTransforms.TransformType.FIRST_PERSON_RIGHT_HAND || cameraTransformType == ItemCameraTransforms.TransformType.FIRST_PERSON_LEFT_HAND) {
101 | return this;
102 | }
103 | return this.existingModel.handlePerspective(cameraTransformType, mat);
104 | }
105 | }
106 | ```
107 |
108 | 可以看到,这里和之前创建的`IBakedModel`并无太大差别。同样是保存了一个原本的`IBakedModel`。
109 |
110 | 这里我们来说说他们区别:
111 |
112 | 第一,对于物品的`IBakedModel`来说,只会调用`IBakedModel#getQuads`而不会调用`IForgeBakedModel::getQuads`,你可以和之前方块的`IBakedModel`做对比,可以发现刚好是相反的。
113 |
114 | 第二,对于物品,你可以通过` handlePerspective`这个方块来选择不同`TransformType`下的`IBakedModel`,具体的`TransformType`请自行翻阅模型相关的Wiki,这里我们希望在第一人称的视角下用`ISTER`渲染我们的模型,所以在`if`语句中返回了`this`,注意只有当你这里返回了`this`的`TransformType`,才会启用`ISTER`渲染。
115 |
116 | 第三,你可以在`getOverrides`处理物品`json`文件中的`Override`,什么是`Overrides`请执行翻阅wiki。
117 |
118 | 第四,也是最重要的,如果你希望你的`IBakedModel`被`ISTER`渲染,你必须在`isBuiltInRenderer`返回`true`,如果你没有给你的物品中调用`setISTER`指定自定义的`ISTER`,默认会使用`ItemStackTileEntityRenderer.instance`渲染。
119 |
120 | 接下就是最为关键的部分,我们的`ISTER`
121 |
122 | ```java
123 | public class ObsidianWrenchItemStackTileEntityRenderer extends ItemStackTileEntityRenderer {
124 | private int degree = 0;
125 |
126 | @Override
127 | public void render(ItemStack itemStackIn, MatrixStack matrixStackIn, IRenderTypeBuffer bufferIn, int combinedLightIn, int combinedOverlayIn) {
128 | if (degree == 360) {
129 | degree = 0;
130 | }
131 | degree++;
132 | ItemRenderer itemRenderer = Minecraft.getInstance().getItemRenderer();
133 | IBakedModel ibakedmodel = itemRenderer.getItemModelWithOverrides(itemStackIn, null, null);
134 | matrixStackIn.push();
135 | matrixStackIn.translate(0.5F, 0.5F, 0.5F);
136 | float xOffset = -1 / 32f;
137 | float zOffset = 0;
138 | matrixStackIn.translate(-xOffset, 0, -zOffset);
139 | matrixStackIn.rotate(Vector3f.YP.rotationDegrees(degree));
140 | matrixStackIn.translate(xOffset, 0, zOffset);
141 | itemRenderer.renderItem(itemStackIn, ItemCameraTransforms.TransformType.NONE, false, matrixStackIn, bufferIn, combinedLightIn, combinedOverlayIn, ibakedmodel.getBakedModel());
142 | matrixStackIn.pop();
143 | }
144 | }
145 | ```
146 |
147 | 可以看到我们的`ISTER`类直接继承了`ItemStackTileEntityRenderer`,`ItemStackTileEntityRenderer`只有一个方法,即`render`方法,这里的`render`方法和之前我们学习过的`TER`里的`render`方法作用类似。你可以在这里渲染,你想要渲染的东西。
148 |
149 | 我只在这里渲染了,我们的物品模型然后在调整完位置之后。让模型旋转起来。
150 |
151 | ```java
152 | float xOffset = -1 / 32f;
153 | float zOffset = 0;
154 | matrixStackIn.translate(-xOffset, 0, -zOffset);
155 | matrixStackIn.rotate(Vector3f.YP.rotationDegrees(degree));
156 | matrixStackIn.translate(xOffset, 0, zOffset);
157 | ```
158 |
159 | 这段就是让模型能按其中轴线旋转的代码,来源是Create Mod 的`WrenchItemRenderer`。
160 |
161 | 因为`render`每一帧都会被调用一次。
162 |
163 | ```java
164 | if (degree == 360) {
165 | degree = 0;
166 | }
167 | degree++;
168 | ```
169 |
170 | 所以你可以利用这样写出平滑的旋转动画。
171 |
172 | 到此,我们的物品特殊渲染就完成了。
173 |
174 | 打开游戏看看,你应该就能看到一个会自动旋转的物品了。
175 |
176 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/ister)
177 |
178 |
--------------------------------------------------------------------------------
/src/worldsaveddata/example.md:
--------------------------------------------------------------------------------
1 | # WorldSavedData(世界数据保存)
2 |
3 | 在这节中,我们将来学习另外一种可以保存数据的方法,这就是World Saved Data。World Saved Data 是Minecraft 提供的一种可以让你保存和共享数据的类。World Saved Data 是在维度级别的,你只能指定某个维度拥有一个WorldSavedData,里面保存的数据是所有玩家通用的,并且用World Saved Data保存的数据与存档数据是分开存放的。
4 |
5 | 在这节中,我们创建一个可以远程传输和储存物品的机器。
6 |
7 | 首先我们来看`ObsidianWorldSavedData`:
8 |
9 | ```java
10 | public class ObsidianWorldSavedData extends WorldSavedData {
11 | private static final String NAME = "ObsidianWorldSavedData";
12 | private Stack itemStacks = new Stack<>();
13 |
14 | public ObsidianWorldSavedData() {
15 | super(NAME);
16 | }
17 |
18 | public boolean putItem(ItemStack item) {
19 | itemStacks.push(item);
20 | markDirty();
21 | return true;
22 | }
23 |
24 | public ItemStack getItem() {
25 | if (itemStacks.isEmpty()) {
26 | return new ItemStack(Items.AIR);
27 | }
28 | markDirty();
29 | return itemStacks.pop();
30 | }
31 |
32 | public ObsidianWorldSavedData(String name) {
33 | super(name);
34 | }
35 |
36 | public static ObsidianWorldSavedData get(World worldIn) {
37 | if (!(worldIn instanceof ServerWorld)) {
38 | throw new RuntimeException("Attempted to get the data from a client world. This is wrong.");
39 | }
40 |
41 | ServerWorld world = worldIn.getServer().getWorld(DimensionType.OVERWORLD);
42 | /***
43 | * 如果你需要每个纬度都有一个自己的World Saved Data。
44 | * 用 ServerWorld world = (ServerWorld)world; 代替上面那句。
45 | */
46 | DimensionSavedDataManager storage = world.getSavedData();
47 | return storage.getOrCreate(() -> {
48 | return new ObsidianWorldSavedData();
49 | }, NAME);
50 | }
51 |
52 | @Override
53 | public void read(CompoundNBT nbt) {
54 | ListNBT listNBT = (ListNBT) nbt.get("list");
55 | if (listNBT != null) {
56 | for (INBT value : listNBT) {
57 | CompoundNBT tag = (CompoundNBT) value;
58 | ItemStack itemStack = ItemStack.read(tag.getCompound("itemstack"));
59 | itemStacks.push(itemStack);
60 | }
61 | }
62 | }
63 |
64 | @Override
65 | public CompoundNBT write(CompoundNBT compound) {
66 | ListNBT listNBT = new ListNBT();
67 | itemStacks.stream().forEach((stack) -> {
68 | CompoundNBT compoundNBT = new CompoundNBT();
69 | compoundNBT.put("itemstack", stack.serializeNBT());
70 | listNBT.add(compoundNBT);
71 | });
72 | compound.put("list", listNBT);
73 | return compound;
74 | }
75 | }
76 | ```
77 |
78 | 首先你需要给你的WorldSavedData命名,请注意这个名字是必须是唯一的,然后在构建函数的`Super`方法里填入这个名字。
79 |
80 | ```java
81 | public ObsidianWorldSavedData() {
82 | super(NAME);
83 | }
84 | public ObsidianWorldSavedData(String name) {
85 | super(name);
86 | }
87 | ```
88 |
89 | 类似如上。
90 |
91 | 接下来就是你要创建这个WorldSavedData,我们来看`get`方法
92 |
93 | ```java
94 | public static ObsidianWorldSavedData get(World worldIn) {
95 | if (!(worldIn instanceof ServerWorld)) {
96 | throw new RuntimeException("Attempted to get the data from a client world. This is wrong.");
97 | }
98 | ServerWorld world = worldIn.getServer().getWorld(DimensionType.OVERWORLD);
99 | DimensionSavedDataManager storage = world.getSavedData();
100 | return storage.getOrCreate(() -> {
101 | return new ObsidianWorldSavedData();
102 | }, NAME);
103 | }
104 | ```
105 |
106 | 首先,所有的存档保存的操作都应该在服务端执行,所以我们要先判断我们执行`get`方法时,是不是在服务端。
107 |
108 | ```java
109 | ServerWorld world = worldIn.getServer().getWorld(DimensionType.OVERWORLD);
110 | ```
111 |
112 | 接下来,我们通过这句话获取了主世界维度相对应的`ServerWorld`类,之所以强制获取主世界相对应的`ServerWorld`原因是,我们得有要实现跨世界传送功能,所以我们把数据统一保存在主世界下的WorldSavedData里。
113 |
114 | 如果你希望每个维度都有自己的WorldSavedData,可以使用下面这条命令代替即可。
115 |
116 | ```java
117 | ServerWorld world = (ServerWorld)world;
118 | ```
119 |
120 | 然后就是调用从`ServerWorld`中获取`DimensionSavedDataManager`,并调用`getOrCreate`来获取或者创建我们的WorldSavedData.
121 |
122 | `getOrCreate`第一个参数返回值是你的WorldSavedData实例,第二个参数就是你指定的名字。
123 |
124 | 这样写完以后,你可以在你需要的地方调用`ObsidianWorldSavedData.get`即可获或者自动创建取我们写好的WorldSavedData了。
125 |
126 | 接下来是,我们自己创建的用来读取和存放数据的接口
127 |
128 | ```java
129 | public boolean putItem(ItemStack item) {
130 | itemStacks.push(item);
131 | markDirty();
132 | return true;
133 | }
134 |
135 | public ItemStack getItem() {
136 | if (itemStacks.isEmpty()) {
137 | return new ItemStack(Items.AIR);
138 | }
139 | markDirty();
140 | return itemStacks.pop();
141 | }
142 | ```
143 |
144 | 我们在这里用了` private Stack itemStacks`来保存我们的数据,当然当你修改完数据之后记得要`markDirty()`不然你修改后的数据不会被保存。
145 |
146 | ```java
147 | @Override
148 | public void read(CompoundNBT nbt) {
149 | ListNBT listNBT = (ListNBT) nbt.get("list");
150 | if (listNBT != null) {
151 | for (INBT value : listNBT) {
152 | CompoundNBT tag = (CompoundNBT) value;
153 | ItemStack itemStack = ItemStack.read(tag.getCompound("itemstack"));
154 | itemStacks.push(itemStack);
155 | }
156 | }
157 | }
158 |
159 | @Override
160 | public CompoundNBT write(CompoundNBT compound) {
161 | ListNBT listNBT = new ListNBT();
162 | itemStacks.stream().forEach((stack) -> {
163 | CompoundNBT compoundNBT = new CompoundNBT();
164 | compoundNBT.put("itemstack", stack.serializeNBT());
165 | listNBT.add(compoundNBT);
166 | });
167 | compound.put("list", listNBT);
168 | return compound;
169 | }
170 | ```
171 |
172 | 然后就是数据的保存和恢复,这里没什么好说的,只是用的了`ListNBT`来保存我们的数据而已。
173 |
174 | 然后我们来看看如何使用这个WorldSavedData吧。
175 |
176 | ```java
177 | public class ObsidianItemSaveBlock extends Block {
178 | public ObsidianItemSaveBlock() {
179 | super(Properties.create(Material.ROCK).hardnessAndResistance(5));
180 | }
181 |
182 | @Override
183 | public ActionResultType onBlockActivated(BlockState state, World worldIn, BlockPos pos, PlayerEntity player, Hand handIn, BlockRayTraceResult hit) {
184 | if (!worldIn.isRemote && handIn == Hand.MAIN_HAND) {
185 | ObsidianWorldSavedData worldSavedData = ObsidianWorldSavedData.get(worldIn);
186 | ItemStack mainHandItemStack = player.getItemStackFromSlot(EquipmentSlotType.MAINHAND);
187 | if (!mainHandItemStack.isEmpty()) {
188 | worldSavedData.putItem(mainHandItemStack.copy());
189 | mainHandItemStack.shrink(mainHandItemStack.getCount());
190 | } else {
191 | ItemStack stack = worldSavedData.getItem();
192 | player.setItemStackToSlot(EquipmentSlotType.MAINHAND, stack);
193 | }
194 | }
195 | return ActionResultType.SUCCESS;
196 | }
197 | }
198 | ```
199 |
200 | 这里最重要的就是`onBlockActivated`方法,同样的我们需要判断代码是不是在服务端执行并且传入的手是不是主手。
201 |
202 | ```java
203 | ObsidianWorldSavedData worldSavedData = ObsidianWorldSavedData.get(worldIn);
204 | ItemStack mainHandItemStack = player.getItemStackFromSlot(EquipmentSlotType.MAINHAND);
205 | ```
206 |
207 | 我们首先获取了之前创建好的WorldSavedData以及主手的ItemStack.
208 |
209 | 然后判断ItemStack是否为空,不为空代表我们需要放东西,为空代表我们需要取东西。
210 |
211 | ```java
212 | worldSavedData.putItem(mainHandItemStack.copy());
213 | mainHandItemStack.shrink(mainHandItemStack.getCount());
214 | ```
215 |
216 | 这里就是我们放东西的代码,调用了我们自定义的`putItem`,来存放数据。请注意,这里传入的必须是`mainHandItemStack.copy()`,不然我们之后减少物品时,我们存入的物品也会变空。
217 |
218 | 而第二句话就是`shrink`「收缩」物品,收缩的数量就是物品的数量。这样我们就把物品变空了。
219 |
220 | ```java
221 | ItemStack stack = worldSavedData.getItem();
222 | player.setItemStackToSlot(EquipmentSlotType.MAINHAND, stack);
223 | ```
224 |
225 | 这句话也很简单,就是取出物品然后还给玩家而已。
226 |
227 | 打开游戏右键你的方块试试吧,现在应该可以不限距离,不限维度传输物品了。
228 |
229 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/wordsaveddata)
--------------------------------------------------------------------------------
/src/capability/capabilityfromscratch.md:
--------------------------------------------------------------------------------
1 | # 从零构建与使用能力
2 |
3 | 在这一节中,我们将会从0开始构建与使用Capability。在开始之前我们必须要解释一下,什么是Capability。
4 |
5 | Capability 的诞生起源于在写程序时出现的一个问题。考虑如下一种情况,你正在写一个能量转换器,你的能量转换器需要可以转换不同mod中的不同能量:EU、RF,Forge Energy等等。每一个能量都提供了一个接口用来实现输入输出的方法,那么你很快就会发现你的类定义会变成下面这个样子:
6 |
7 | ```java
8 | class MyTileEntity extends TileEntity implements EnergyInterface1, EnergyInterface2, EnergyInterface3, FluidsInterface1, FluidsInterface2, FluidsInterface3, ItemsApi1, ItemsApi2, ItemsApi3, ComputerApi1, ComputerApi2, ...
9 | ```
10 |
11 | 这样的代码简直是地狱级别的可怕。而Capability(能力系统)出现解决了这个问题,再用了能力系统之后你的类定义就可以简化成类似下面的形式:
12 |
13 | ```java
14 | class MyTileEntity extends CapabilityProvider
15 | ```
16 |
17 | 这里是演示代码,但是你可以立马发现,你的代码简化了很多。请注意Capability系统并不是Minecraft原版提供的,而是Forge提供的。
18 |
19 | 在Capability系统主要由两部分构成,Capability(能力)本身以及CapabilityProvider(能力提供者)。请注意务必要区分清楚Capability和CapabilityProvider,在很多的教程中,这两者都被混为一谈。当然在实际使用的过程中还会牵涉到调用CapabilityProvider的一方。这里我讲一个简单比喻来帮助大家理解这三方的关系。
20 |
21 | > 假设,你是一个投资商,想要投资建一栋商务楼。一般情况下,你就会去找一个建筑方案提供商。如果这个建筑方案提供商拿不出这个建筑方案,你可能就不建了,如果这个建筑方案提供商拿的出建筑方案,你就可以从这个方法中获取一些信息或者直接按照这个方案建筑商务楼。
22 |
23 | 在Capability系统中,调用CapabilityProvider的方相当于「投资商」,CapabilityProvider相当于是「建筑方案提供商」,而Capability本身就是「建筑方案」。
24 |
25 | 而在代码层面,首先调用方会调用CapabilityProvider的`getCapability`方法并传入一个具体的Capability。然后在实现了`CapbilityProvider`接口的类中,判断传入的能力是不是自己可以完成的,如果不能换成就会返回一个空值,如果可以完成就会返回传入的Capability所指定类或接口的一个实例。
26 |
27 | 接下来我们会创建两个特殊的方块,分为「上方块」和「下方块」。当你把「上方块」放在「下方块」的上方时,「上方块会输出下方块传来的信息」。
28 |
29 | 介于篇幅的原因,这里就直接从TileEntity开始了,相关的方块也没什么内容,就只是关联的方块实体而已。
30 |
31 | `ObsidianUpBlockTileEntity`
32 |
33 | ```java
34 | public class ObsidianUpBlockTileEntity extends TileEntity implements ITickableTileEntity {
35 | public ObsidianUpBlockTileEntity() {
36 | super(TileEntityTypeRegistry.obsidianUpTileEntity.get());
37 | }
38 |
39 | private static Logger logger = LogManager.getLogger();
40 |
41 | @Override
42 | public void tick() {
43 | if (!world.isRemote) {
44 | BlockPos pos = this.pos.down();
45 | TileEntity tileEntity = world.getTileEntity(pos);
46 | if (tileEntity != null) {
47 | LazyOptional simpleCapabilityLazyOptional = tileEntity.getCapability(ModCapability.SIMPLE_CAPABILITY);
48 | simpleCapabilityLazyOptional.ifPresent((s) -> {
49 | String context = s.getString(this.pos);
50 | logger.info(context);
51 | });
52 | }
53 | }
54 |
55 | }
56 | }
57 | ```
58 |
59 | 可以看见这里的内容并不艰深。我们先获取了下方方块的方块实体。
60 |
61 | ```java
62 | LazyOptional simpleCapabilityLazyOptional = tileEntity.getCapability(ModCapability.SIMPLE_CAPABILITY);
63 | ```
64 |
65 | 然后就是调用这里的`getCapbility`并传入了我们自定义的`ModCapability.SIMPLE_CAPABILITY`这个能力,来询问我们下方的方块是否可以完成这件事。
66 |
67 | 这里有个很奇怪的类型,叫做`LazyOptional`我们必须要稍微解释一下。简单来说你在读这个类的名字时可以忽视`Lazy`,它其实就是一个特殊的`Optional`类。那么什么是`Optioanl`类呢?`Optinal` 类其实是对`null`和`有值`这两个状态的封装。简单来说,当`LazyOptional.isPresent`为真时,代表这个变量里有值,如果为假,代表这个变量的值为`null`。
68 |
69 | ```java
70 | simpleCapabilityLazyOptional.ifPresent((s) -> {
71 | String context = s.getString(this.pos);
72 | logger.info(context);
73 | });
74 | ```
75 |
76 | 这里的`ifPresent`的意思就是当返回值不是`null`时需要做什么。这里的lambda表达式参数`s`就是之前`getCapbility`调用的返回值。而且它的类型就是我们写在`LazyOptional`这个泛型中的类型,在我们的例子里就是`ISimpleCapability`这个接口的实例。
77 |
78 | 然后我们就调用了`ISimpleCapability`规定的方法`getString`,并且把值输出了出来。
79 |
80 | 读完这里希望读者能稍微停一下,然后思考这个过程和之前比喻之间的关系。
81 |
82 | 接下来我们就要来看看我们的「方案提供商人 / CapabilityProvider 」了。
83 |
84 | `ObsidianDownBlockTileEntity.java`
85 |
86 | ```java
87 | public class ObsidianDownBlockTileEntity extends TileEntity {
88 | public ObsidianDownBlockTileEntity() {
89 | super(TileEntityTypeRegistry.obsidianDownTileEntity.get());
90 | }
91 |
92 | @Nonnull
93 | @Override
94 | public LazyOptional getCapability(@Nonnull Capability cap, @Nullable Direction side) {
95 | if (cap == ModCapability.SIMPLE_CAPABILITY) {
96 | return LazyOptional.of(() -> {
97 | return new SimpleCapability("Hello");
98 | }).cast();
99 | }
100 | return LazyOptional.empty();
101 | }
102 | }
103 | ```
104 |
105 | 可以看见我们这里就简单的复写了`getCapability`方法,你可能会好奇`getCapability`明明是`CapabilityProvider`规定的方法,我们却可以直接复写。
106 |
107 | 如果你查看TileEntity类的定义,你应该可以看到如下内容:
108 |
109 | ```java
110 | public abstract class TileEntity extends net.minecraftforge.common.capabilities.CapabilityProvider implements net.minecraftforge.common.extensions.IForgeTileEntity
111 | ```
112 |
113 | 可以看见TileEntity类默认继承了CapabilityProvider(知道Capability对于方块实体多么重要了吧),这就是为什么我们可以直接复写`getCapability`的原因。
114 |
115 | 这里的内容,非常简单,我们就是判断传入的能力是不是我们可以完成的,如果不可以完成就返回一个`LazyOptional.empty()`(你可以当成返回了一个`null`),如果可以完成就返回了一个相对应的实例。
116 |
117 | ```java
118 | return LazyOptional.of(() -> {
119 | return new SimpleCapability("Hello");
120 | }).cast();
121 | ```
122 |
123 | 这里的意思是将`SimpleCapability`的实例塞入`LazyOptinal`中。请注意,最后的这里的`cast`方法是必须要调用的,不然会出现类型错误。
124 |
125 | 这里的`SimpleCapability`就是实现了我们之前提到的`ISimpleCapability`接口的类。所以这里返回的实例也是`ISimpleCapability`的实例。
126 |
127 | 接下来我们来看`SimpleCapability.java`和`ISimpleCapability.java`
128 |
129 | ```java
130 | public class SimpleCapability implements ISimpleCapability {
131 | private String context;
132 |
133 | public SimpleCapability(String context) {
134 | this.context = context;
135 | }
136 |
137 | @Override
138 | public String getString(BlockPos pos) {
139 | return pos.toString() + ":::" + this.context;
140 | }
141 | }
142 | ```
143 |
144 | ```java
145 | public interface ISimpleCapability {
146 | String getString(BlockPos pos);
147 | }
148 | ```
149 |
150 | 在你写自定义Capability(这里指的不是CapabilityProvider)时,你应该也要遵循这个形式,把规定的操作抽象成一个接口,这样你就可以你就可以在Mod的API中向别人提供这个接口,别人就可以在它们的Mod中使用你定义的Capability了。
151 |
152 | 接下来是注册,首先我们得声明一个实例。
153 |
154 | `ModCapability.java`
155 |
156 | ```java
157 | public class ModCapability {
158 | @CapabilityInject(ISimpleCapability.class)
159 | public static Capability SIMPLE_CAPABILITY;
160 | }
161 | ```
162 |
163 | 在这里我们使用了`@CapabilityInject`注解,来表示我们的定义的变量是Capability,并且会在正式注册完这个Capability后给下面的变量赋值。这里的参数和下方的泛型,都应该是你之前定义的Capability的接口。
164 |
165 | 最后我们来正式注册它。
166 |
167 | ```java
168 | @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
169 | public class CommonSetupEventHandler {
170 | @SubscribeEvent
171 | public static void onSetupEvent(FMLCommonSetupEvent event) {
172 | CapabilityManager.INSTANCE.register(
173 | ISimpleCapability.class,
174 | new Capability.IStorage() {
175 | @Nullable
176 | @Override
177 | public INBT writeNBT(Capability capability, ISimpleCapability instance, Direction side) {
178 | return null;
179 | }
180 |
181 | @Override
182 | public void readNBT(Capability capability, ISimpleCapability instance, Direction side, INBT nbt) {
183 |
184 | }
185 | },
186 | () -> null
187 | );
188 | }
189 | }
190 | ```
191 |
192 | 你需要在`Mod`总线的`FMLCommonSetupEvent`(通用启动设置)这个生命周期事件发生时,调用` CapabilityManager.INSTANCE.register`注册你的Capability,这里第一个参数就是你写好的Capability,后面两个参数你是用来规定默认情况下的储存和创建方法的,我们不需要这些功能,所以留空。你可直接像上面的代码一样写最后两个参数。
193 |
194 | 然后就是方块,方块实体等的注册,这里就不多加解释了。
195 |
196 | 打开游戏。
197 |
198 | 
199 |
200 | 当像这样叠放时。
201 |
202 | 
203 |
204 | 控制台里输出了相对应的内容了。
205 |
206 | 这部分的概念有些抽象,大家可以结合源代码自己思考整个调用过程。只要你理解了调用过程,Capability系统其实没有想象的那么难以理解。
207 |
208 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/first_cap)
209 |
210 |
--------------------------------------------------------------------------------
/src/capability/attachcapabilityprovider.md:
--------------------------------------------------------------------------------
1 | # 附加能力提供器
2 |
3 | 在之前的章节中,我们已经学习了如何使用能力系统,已经知道了如何从零创建一个能力。在这节中,我们将要来学习,如何为现有的实体、物品等附加能力提供器。
4 |
5 | 在这节中,我们会给玩家添加一个新的CapabilityProvider,让玩家可以拥有一套新的等级系统。
6 |
7 | 首先我们来创建一个能力,这个能力非常简单。
8 |
9 | ```java
10 | public interface ISpeedUpCapability extends INBTSerializable {
11 | int getLevel();
12 | }
13 | ```
14 |
15 | ```java
16 | public class SpeedUpCapability implements ISpeedUpCapability {
17 | private int level;
18 |
19 | public SpeedUpCapability(int level) {
20 | this.level = level;
21 | }
22 |
23 | @Override
24 | public int getLevel() {
25 | return level;
26 | }
27 |
28 | @Override
29 | public CompoundNBT serializeNBT() {
30 | CompoundNBT compoundNBT = new CompoundNBT();
31 | compoundNBT.putInt("level", this.level);
32 | return compoundNBT;
33 | }
34 |
35 | @Override
36 | public void deserializeNBT(CompoundNBT nbt) {
37 | this.level = nbt.getInt("level");
38 | }
39 | }
40 | ```
41 |
42 | 可以看见我们让我们的Capability实现了`INBTSerializable`接口,之所以要这么做的原因是,我们之后需要保存和恢复数据。**请注意,虽然这里的Capability实现了`INBTSerializable`,但是实际保存数据的地方并不在Capability中,而是在CapabilityProvider中,请务必区分**
43 |
44 | 上面的内容非常好懂,就不多加说明了。
45 |
46 | 不要忘了创建与注册能力。
47 |
48 | ```java
49 | @CapabilityInject(ISpeedUpCapability.class)
50 | public static Capability SPEED_UP_CAPABILITY;
51 | ```
52 |
53 | ```java
54 | CapabilityManager.INSTANCE.register(
55 | ISpeedUpCapability.class,
56 | new Capability.IStorage() {
57 | @Nullable
58 | @Override
59 | public INBT writeNBT(Capability capability, ISpeedUpCapability instance, Direction side) {
60 | return null;
61 | }
62 |
63 | @Override
64 | public void readNBT(Capability capability, ISpeedUpCapability instance, Direction side, INBT nbt) {
65 |
66 | }
67 | },
68 | () -> null
69 | )
70 | ```
71 |
72 | 接下来就是重头戏,我们来创建一个新的CapabilityProvider
73 |
74 | ```java
75 | public class SpeedUpCapabilityProvider implements ICapabilityProvider, INBTSerializable {
76 | private ISpeedUpCapability speedUpCapability;
77 |
78 | @Nonnull
79 | @Override
80 | public LazyOptional getCapability(@Nonnull Capability cap, @Nullable Direction side) {
81 | return cap == ModCapability.SPEED_UP_CAPABILITY ? LazyOptional.of(() -> {
82 | return this.getOrCreateCapability();
83 | }).cast() : LazyOptional.empty();
84 | }
85 |
86 | @Nonnull
87 | ISpeedUpCapability getOrCreateCapability() {
88 | if (speedUpCapability == null) {
89 | Random random = new Random();
90 | this.speedUpCapability = new SpeedUpCapability(random.nextInt(99) + 1);
91 | }
92 | return this.speedUpCapability;
93 | }
94 |
95 | @Override
96 | public CompoundNBT serializeNBT() {
97 | return getOrCreateCapability().serializeNBT();
98 | }
99 |
100 | @Override
101 | public void deserializeNBT(CompoundNBT nbt) {
102 | getOrCreateCapability().deserializeNBT(nbt);
103 | }
104 | }
105 | ```
106 |
107 | 可以看到,这里我们实现了两个接口`ICapabilityProvider`和`INBTSerializable`,其中`ICapabilityProvider`接口是必须实现的,而`INBTSerializable`是可以不用实现的,如果你的CapabilityProvider不需要保存数据,你可以不实现这个接口,如果你实现了这个接口,当你附加完能力时,Forge在保存数据和读取数据时,会自动调用这两个接口。
108 |
109 | 我们在这里直接调用了,我们之前实现的Capability内相对应的方法。
110 |
111 | `getOrCreateCapability`内容很简单,就是如果没有创建能力就创建一个新的能力,如果有就返回一个旧的能力,并且在创建新的能力时,随机给予一个等级。
112 |
113 | 接下来我们需要讲我们的能力附加到玩家身上。
114 |
115 | ```java
116 | @Mod.EventBusSubscriber()
117 | public class CommonEventHandler {
118 | @SubscribeEvent
119 | public static void onAttachCapabilityEvent(AttachCapabilitiesEvent event) {
120 | Entity entity = event.getObject();
121 | if (entity instanceof PlayerEntity) {
122 | event.addCapability(new ResourceLocation("neutrino", "speedup"), new SpeedUpCapabilityProvider());
123 | }
124 | }
125 |
126 | @SubscribeEvent
127 | public static void onPlayerCloned(PlayerEvent.Clone event) {
128 | if (!event.isWasDeath()) {
129 | LazyOptional oldSpeedCap = event.getOriginal().getCapability(ModCapability.SPEED_UP_CAPABILITY);
130 | LazyOptional newSpeedCap = event.getPlayer().getCapability(ModCapability.SPEED_UP_CAPABILITY);
131 | if (oldSpeedCap.isPresent() && newSpeedCap.isPresent()) {
132 | newSpeedCap.ifPresent((newCap) -> {
133 | oldSpeedCap.ifPresent((oldCap) -> {
134 | newCap.deserializeNBT(oldCap.serializeNBT());
135 | });
136 | });
137 | }
138 | }
139 | }
140 | }
141 | ```
142 |
143 | 我们先来看`onAttachCapabilityEvent` 这个事件处理器。
144 |
145 | ```java
146 | @SubscribeEvent
147 | public static void onAttachCapabilityEvent(AttachCapabilitiesEvent event) {
148 | Entity entity = event.getObject();
149 | if (entity instanceof PlayerEntity) {
150 | event.addCapability(new ResourceLocation("neutrino", "speedup"), new SpeedUpCapabilityProvider());
151 | }
152 | }
153 | ```
154 |
155 | 这里我们监听了`AttachCapabilitiesEvent`事件,我们可以通过这个事件来为特定的对象附加自定义的CapabilityProvider。这里我们就简单了判断了实体是不是玩家,如果是玩家,就附加能力。**请注意:**`event.addCapability`方法,看上去附加的好像不是CapabilityProvider而是Capability,但如果你观察过它的函数签名,你会发现第二个参数需要的类型是`ICapabilityProvider`,所以在一开始你可以把这个函数理解成成`event.addCapabilityProvider`。这里的第一个参数是一个标记符,每一个附加的CapabilityProvider都必须是唯一的,`ResourceLocation`第一个参数一般情况填入你的`modId`,后一个参数随你喜好填。
156 |
157 |
158 |
159 | - Entity
160 | - TileEntity
161 | - ItemStack
162 | - World
163 | - Chunk
164 |
165 | 接下来我们来看
166 |
167 | ```java
168 | @SubscribeEvent
169 | public static void onPlayerCloned(PlayerEvent.Clone event) {
170 | if (!event.isWasDeath()) {
171 | LazyOptional oldSpeedCap = event.getOriginal().getCapability(ModCapability.SPEED_UP_CAPABILITY);
172 | LazyOptional newSpeedCap = event.getPlayer().getCapability(ModCapability.SPEED_UP_CAPABILITY);
173 | if (oldSpeedCap.isPresent() && newSpeedCap.isPresent()) {
174 | newSpeedCap.ifPresent((newCap) -> {
175 | oldSpeedCap.ifPresent((oldCap) -> {
176 | newCap.deserializeNBT(oldCap.serializeNBT());
177 | });
178 | });
179 | }
180 | }
181 | }
182 | ```
183 |
184 | 当玩家死亡后重生或者从末地回到主世界,都会触发这个方法,理论上来说从末地回到主世界应该会自动同步数据,不知道处于什么样子的考虑,这个功能一直没有实现,所以我们需要在这里手动的恢复我们的能力。`event.isWasDeath()` 为真时代表玩家死亡后重生,而为假时代表从末地回到主世界。在这里`event.getOriginal()` 得到的是玩家之前的实体,`event.getPlayer()`代表的是玩家重生之后的实体。
185 |
186 | `newCap.deserializeNBT(oldCap.serializeNBT());`我们在这里恢复了数据,这就是为什么我们需要让我们的Capability也实现`INBTSerializable`的原因。
187 |
188 | 最后就是使用能力了
189 |
190 | ```java
191 | public class ObsidianSpeedUpShowItem extends Item {
192 | public ObsidianSpeedUpShowItem() {
193 | super(new Properties().group(ModGroup.itemGroup));
194 | }
195 |
196 | @Override
197 | public ActionResult onItemRightClick(World worldIn, PlayerEntity playerIn, Hand handIn) {
198 | if (!worldIn.isRemote && handIn == Hand.MAIN_HAND) {
199 | LazyOptional speedCap = playerIn.getCapability(ModCapability.SPEED_UP_CAPABILITY);
200 | speedCap.ifPresent((l) -> {
201 | int level = l.getLevel();
202 | playerIn.sendMessage(new StringTextComponent("Level: " + level));
203 | }
204 | );
205 | }
206 | return super.onItemRightClick(worldIn, playerIn, handIn);
207 | }
208 | }
209 | ```
210 |
211 | 没什么好说的,我们只是从Capability从获取了等级并且输出到了聊天框而已。
212 |
213 | 
214 |
215 | 打开游戏,右键相对应的物品,你就能看到等级被显示了。
216 |
217 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/entity_capabilityprovider)
218 |
219 | ## 编程小课堂
220 |
221 | 在写Mod的时候,你必须学会看源代码和Debug(比如下断点)等。这些是基本技能。
--------------------------------------------------------------------------------
/src/worldgeneration/structuregeneration.md:
--------------------------------------------------------------------------------
1 | # 结构生成
2 |
3 | 在这节中我们将来学习如何创建一个自定义的结构,并且在世界中自动生成它。我们将以钻石小屋作为例子。
4 |
5 | 首先既然我们的要自定义的是一个结构,那么也就需要创建一个结构,内容如下:
6 |
7 | ```java
8 | public class DiamondHouseStructure extends Structure {
9 | public DiamondHouseStructure(Function, ? extends NoFeatureConfig> configFactoryIn) {
10 | super(configFactoryIn);
11 | }
12 |
13 | @Override
14 | public boolean canBeGenerated(BiomeManager biomeManagerIn, ChunkGenerator> generatorIn, Random randIn, int chunkX, int chunkZ, Biome biomeIn) {
15 | if (randIn.nextFloat() < 0.03) {
16 | return true;
17 | }
18 | return false;
19 | }
20 |
21 | @Override
22 | public IStartFactory getStartFactory() {
23 | return (structure, chunkPosX, chunkPosZ, bounds, references, seed) -> {
24 | return new Start(structure, chunkPosX, chunkPosZ, bounds, references, seed);
25 | };
26 | }
27 |
28 | @Override
29 | public String getStructureName() {
30 | return "neutrino_house";
31 | }
32 |
33 | @Override
34 | public int getSize() {
35 | return 3;
36 | }
37 |
38 | public static class Start extends StructureStart {
39 |
40 | public Start(Structure> structure, int chunkPosX, int chunkPosZ, MutableBoundingBox bounds, int references, long seed) {
41 | super(structure, chunkPosX, chunkPosZ, bounds, references, seed);
42 | }
43 |
44 | @Override
45 | public void init(ChunkGenerator> generator, TemplateManager templateManagerIn, int chunkX, int chunkZ, Biome biomeIn) {
46 | DiamondHouseStructurePiece diamondHouseStructurePiece = new DiamondHouseStructurePiece(this.rand, chunkX * 16, chunkZ * 16);
47 | this.components.add(diamondHouseStructurePiece);
48 | this.recalculateStructureSize();
49 | }
50 | }
51 | }
52 | ```
53 |
54 | 首先可以看见我们的`DiamondHouseStructure`继承了`Structure`,这里的`NoFeatureConfig`表明了我们的结构是不需要配置文件的。
55 |
56 | 其中`canBeGenerated`代表了结构会生成的可能性,这里我们设置为3%。`getStructureName`代表了结构的名字,`getSize`具体作用不明,大部分原版结构值都为3。
57 |
58 | 接下来是`Start`类。
59 |
60 | ```java
61 | public static class Start extends StructureStart {
62 |
63 | public Start(Structure> structure, int chunkPosX, int chunkPosZ, MutableBoundingBox bounds, int references, long seed) {
64 | super(structure, chunkPosX, chunkPosZ, bounds, references, seed);
65 | }
66 |
67 | @Override
68 | public void init(ChunkGenerator> generator, TemplateManager templateManagerIn, int chunkX, int chunkZ, Biome biomeIn) {
69 | DiamondHouseStructurePiece diamondHouseStructurePiece = new DiamondHouseStructurePiece(this.rand, chunkX * 16, chunkZ * 16);
70 | this.components.add(diamondHouseStructurePiece);
71 | this.recalculateStructureSize();
72 | }
73 | }
74 | ```
75 |
76 | 这个类的`init`方法就是你添加`StructurePiece`(结构组件)的地方,所谓的`StructurePiece`就是一个结构的组成部分,一个村庄可以有不同的房子组成,每一个房子都是`村庄`这个结构的`StructurePiece`。当然我们在`getStartFactory`这个方法里,返回了构造这个类的方法。
77 |
78 | 接下来我们来看`init`方法。
79 |
80 | 在`init`方法里最要的是这两句话:
81 |
82 | ```java
83 | DiamondHouseStructurePiece diamondHouseStructurePiece = new DiamondHouseStructurePiece(this.rand, chunkX * 16, chunkZ * 16);
84 | this.components.add(diamondHouseStructurePiece);
85 | ```
86 |
87 | 首先我们创建了一个自定义的`StructurePiece`,然后将它添加到了`Structure`自带的`components`中,也就是给我们的结构添加了一个结构组件。
88 |
89 | 最后的`recalculateStructureSize`,用于重新计算结构的边界大小,需要填写。
90 |
91 | 接下来我们来看看`DiamondHouseStructurePiece`具体的内容。
92 |
93 | ```java
94 | public class DiamondHouseStructurePiece extends ScatteredStructurePiece {
95 | private static final DiamondHouseStructurePiece.Selector BUILD_STONE_SELECTOR = new DiamondHouseStructurePiece.Selector();
96 |
97 | protected DiamondHouseStructurePiece(Random random, int x, int z) {
98 | super(CommonEventHandler.diamondHouseStructurePieceType, random, x, 64, z, 12, 10, 15);
99 | }
100 |
101 | protected DiamondHouseStructurePiece(TemplateManager templateManager, CompoundNBT nbt) {
102 | super(CommonEventHandler.diamondHouseStructurePieceType, nbt);
103 | }
104 |
105 | @Override
106 | public boolean create(IWorld worldIn, ChunkGenerator> chunkGeneratorIn, Random randomIn, MutableBoundingBox mutableBoundingBoxIn, ChunkPos chunkPosIn) {
107 | this.fillWithRandomizedBlocks(worldIn, mutableBoundingBoxIn, 0, 0, 0, 4, 4, 4, false, randomIn, BUILD_STONE_SELECTOR);
108 | this.fillWithAir(worldIn, mutableBoundingBoxIn, 1, 1, 1, 3, 3, 3);
109 | this.setBlockState(worldIn, Blocks.ACACIA_TRAPDOOR.getDefaultState().rotate(Rotation.CLOCKWISE_90), 2, 2, 0, mutableBoundingBoxIn);
110 | this.fillWithAir(worldIn, mutableBoundingBoxIn, 2, 1, 0, 2, 1, 0);
111 | return true;
112 | }
113 |
114 | static class Selector extends StructurePiece.BlockSelector {
115 | private Selector() {
116 | }
117 |
118 | public void selectBlocks(Random rand, int x, int y, int z, boolean wall) {
119 | this.blockstate = Blocks.DIAMOND_BLOCK.getDefaultState();
120 | }
121 | }
122 | }
123 | ```
124 |
125 | 可以看到`DiamondHouseStructurePiece`继承了`ScatteredStructurePiece`类,这个类是`StructurePiece`子类,`StructurePiece`还有其他的子类,大家可以按需选用,比如其中的`TemplateStructurePiece`就可以让你从指定的NBT文件中加载模型。
126 |
127 | 我们回到我们的类,可以看到在构造方法里有一个`CommonEventHandler.diamondHouseStructurePieceType`,这个是我们之后需要注册的内容。
128 |
129 | 这里面最为重要的就是`create`,这里也是你「画」建筑的地方。`StructurePiece`提供了一系列的方法,来填充和绘制方块,大家自己看这些函数的签名就能知道其作用了。
130 |
131 | 这里还有一个内部类是`Selector` 它继承了`StructurePiece.BlockSelector`,它的让你可以随机的设置方块的种类的方块的状态,比如原版的丛林神庙,它的墙面的方块种类就是不唯一的,你可以通过这个类的`selectBlocks`实现同样的效果。
132 |
133 | 到此,我们的结构算是创建好了,接下来注册它
134 |
135 | 首先我们注册`Structure`。
136 |
137 | ```java
138 | public class FeatureRegistry {
139 | public static final DeferredRegister> FEATURES = new DeferredRegister<>(ForgeRegistries.FEATURES, "neutrino");
140 | public static RegistryObject> obsidianBlock = FEATURES.register("house", () -> {
141 | return new DiamondHouseStructure(Dynamic -> {
142 | return NoFeatureConfig.deserialize(Dynamic);
143 | });
144 | });
145 | }
146 | ```
147 |
148 | 这里和物品、方块的注册没什么太大的区别,值得注意的是,因为`Structure`其实只是一种特殊的`Feature`,所以我们填的是`DeferredRegister>`。
149 |
150 | 然后又因为我们的`DiamondHouseStructure`是没有配置文件的,所以传入了一个`NoFeatureConfig.deserialize`。
151 |
152 | 同样的,别忘了在你的Mod主类中将`FEATURES`注册到Mod总线中。
153 |
154 | 接下来我们看`StructurePiece`的注册。
155 |
156 | ```java
157 | @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
158 | public class CommonEventHandler {
159 | public static IStructurePieceType diamondHouseStructurePieceType;
160 | @SubscribeEvent
161 | public static void onCommonSetup(FMLCommonSetupEvent event) {
162 | diamondHouseStructurePieceType = Registry.register(Registry.STRUCTURE_PIECE, "house", (templateManager, nbt) -> {
163 | return new DiamondHouseStructurePiece(templateManager, nbt);
164 | });
165 | for (Biome biome : ForgeRegistries.BIOMES) {
166 | biome.addStructure(FeatureRegistry.obsidianBlock.get().withConfiguration(IFeatureConfig.NO_FEATURE_CONFIG));
167 | biome.addFeature(GenerationStage.Decoration.SURFACE_STRUCTURES, FeatureRegistry.obsidianBlock.get().withConfiguration(IFeatureConfig.NO_FEATURE_CONFIG).withPlacement(Placement.NOPE.configure(IPlacementConfig.NO_PLACEMENT_CONFIG)));
168 | }
169 | }
170 | }
171 | ```
172 |
173 | 这里也很简单,首先我们在外边创建一个一个变量,注意类型是`IStructurePieceType`,然后在`FMLCommonSetupEvent`事件中调用`Registry.register`方法注册,因为我们要注册的是`StructurePiece`,所以第一个参数填入的是`Registry.STRUCTURE_PIECE`。
174 |
175 | 然后就和矿物生成的步骤类似添加结构。但是这里请注意,我们需要调用两个方法首先我们得调用`addStructure`添加结构,然后调用`addFeature`让这个结构可以生成,因为我们的`Structure`是`NoFeatureConfig`的,所以`withConfiguration`和`withPlacement`都填入相对应的`NoFeatureConfig`就行。
176 |
177 | 到此,结构已经注册和添加完毕,打开游戏,新建一个存档看看,你应该就能发现世界中自然生成的钻石小屋了。
178 |
179 | 
180 |
181 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/strcutre)
182 |
183 | ## 编程小课堂
184 |
185 | 当你不知道应该如何使用一个类时,可以去Github上搜索看看如何使用,一般情况下都能找到别人使用的例子。
--------------------------------------------------------------------------------
/src/networking/custompack.md:
--------------------------------------------------------------------------------
1 | # 自定义网络包
2 |
3 | 在这节中我们将要来学习如何自定义数据包。在之前的内容中我们已经讲过了如何利用Minecraft原版内置的功能来实现数据的同步,但是这些功能或多或少都有限制。在某些时候我们不得不自己来解决数据同步的问题,在这时我们就得使用自定义数据包了。幸运的是Forge已经提供了一个类,让我们能够简单而且方便地自定义数据包,这个类就是`SimpleChannel`,我们将学习如何使用它。
4 |
5 | 这一节的内容可能有些无聊,因为我们做出的物品并不能在游戏中产生实际的效果,但是这不代表的这节的内容不重要,让我们开始吧。
6 |
7 | 首先我们讲一下发包的过程,具体过程如下:
8 |
9 | ```
10 | 构建数据包=>序列化成字节流 ====通过本地、局域网传输或者因特网传输===> 反序列化成实例 => 实现操作
11 | ```
12 |
13 | 在这个过程中,前两项是在你要发送包的端执行的,后两项是在你要接受包的端执行的。
14 |
15 | 无论你是想从客户端往服务端发包,还是从服务端往客户端发包,你要做的第一件事就是——构建数据包,这个数据包就是你要发送的具体消息。接下来是序列化成字节流,因为在网络中所有的数据都是以字节流的形式传输的,你的数据包也不例外,而又因为数据包的内容是你自己定义的,你得自己实现序列化操作。
16 |
17 | 接下来数据流已经传输到了接受包的一端,你在接受时,接收的同样也是字节流,你需要从字节流中获取数据并恢复数据,重建你的数据包。让你的数据包恢复成功,接下来你就可以利用这些数据来执行操作了。
18 |
19 | 在Mod开发里,所有的数据包都是通过`SimpleChannel`管理的,正如这个名字暗示的那样,我们构建的数据包将通过一个个自定义的「Channel(频道)」传输。
20 |
21 | 接下来我们来注册数据包`Networking`:
22 |
23 | ```java
24 | package com.tutorial.neutrino.network;
25 |
26 | import net.minecraft.util.ResourceLocation;
27 | import net.minecraftforge.fml.network.NetworkRegistry;
28 | import net.minecraftforge.fml.network.simple.SimpleChannel;
29 |
30 | public class Networking {
31 | public static SimpleChannel INSTANCE;
32 | public static final String VERSION = "1.0";
33 | private static int ID = 0;
34 |
35 | public static int nextID() {
36 | return ID++;
37 | }
38 |
39 | public static void registerMessage() {
40 | INSTANCE = NetworkRegistry.newSimpleChannel(
41 | new ResourceLocation("neutrino", "first_networking"),
42 | () -> {
43 | return VERSION;
44 | },
45 | (version) -> {
46 | return version.equals(VERSION);
47 | },
48 | (version) -> {
49 | return version.equals(VERSION);
50 | });
51 | INSTANCE.registerMessage(
52 | nextID(),
53 | SendPack.class,
54 | (pack, buffer) -> {
55 | pack.toBytes(buffer);
56 | },
57 | (buffer) -> {
58 | return new SendPack(buffer);
59 | },
60 | (pack, ctx) -> {
61 | pack.handler(ctx);
62 | }
63 | );
64 | }
65 | }
66 |
67 | ```
68 |
69 |
70 |
71 | ```java
72 | INSTANCE = NetworkRegistry.newSimpleChannel(
73 | new ResourceLocation("neutrino", "first_networking"),
74 | () -> {
75 | return VERSION;
76 | },
77 | (version) -> {
78 | return version.equals(VERSION);
79 | },
80 | (version) -> {
81 | return version.equals(VERSION);
82 | });
83 | ```
84 |
85 | 首先我们创建了一个SimpleChannel的实例,这个实例就是我们之后发包时需要操作的对象。
86 |
87 | 他有如下几个参数,第一个参数`ResourceLocation`是这个`SimpleChannle`的唯一标识符,因为一个mod里可以有许多个传送数据用的`SimplieChannle`,所以需要这个标识符。第二个参数是个匿名函数,返回值是数据包的版本,第三、四个参数是用来控制客户端和服务端可以接收的版本号的,这里的version就是具体的版本号,我们在这里判断是否和当前的版本号相同。如上我们的频道就已经构建完成了。
88 |
89 | 接下来我们来注册数据包。
90 |
91 | ```java
92 | INSTANCE.registerMessage(
93 | nextID(),
94 | SendPack.class,
95 | (pack, buffer) -> {
96 | pack.toBytes(buffer);
97 | },
98 | (buffer) -> {
99 | return new SendPack(buffer);
100 | },
101 | (pack,ctx) ->{
102 | pack.handler(ctx);
103 | }
104 | );
105 | ```
106 |
107 | 这个注册方法有5个参数,我们一一来说明,第一个参数是数据包的序号,这个数据包序号不能重复,所以我们写了一个自增的函数来提供序号。第二个是一个类,这个类就是我们要自定义数据包的类,第三个参数是用来让我们序列化(把数据包实例转换成字节流)我们的数据包,这里我们不需要返回任何值,其中的pack参数就是我们第二个参数中提供的类的一个实例。第四个参数是用来反序列化数据包的(从字节流构建数据包实例),这里我们直接调用了一个特殊的构造方法,然后返回了实例。最后一个参数是用来当接受到数据之后进行一系列操作的,这里的ctx是用来进行线程安全操作用的,至于是什么我们之后再讲。
108 |
109 | 当然像上面那样写实在过于麻烦,其实你可以省略成下面的形式。
110 |
111 | ```java
112 | public class Networking {
113 | public static SimpleChannel INSTANCE;
114 | private static int ID = 0;
115 |
116 | public static int nextID() {
117 | return ID++;
118 | }
119 |
120 | public static void registerMessage() {
121 | INSTANCE = NetworkRegistry.newSimpleChannel(
122 | new ResourceLocation("neutrino" + ":first_networking"),
123 | () -> "1.0",
124 | (s) -> true,
125 | (s) -> true
126 | );
127 | INSTANCE.registerMessage(
128 | nextID(),
129 | SendPack.class,
130 | SendPack::toBytes,
131 | SendPack::new,
132 | SendPack::handler
133 | );
134 | }
135 | }
136 | ```
137 |
138 | 但是为了读者的理解方便,我还是保留上面的形式。
139 |
140 | 接下来就是我们自定义的数据包了`SendPack.java`:
141 |
142 | ```java
143 | public class SendPack {
144 | private String message;
145 | private static final Logger LOGGER = LogManager.getLogger();
146 |
147 | public SendPack(PacketBuffer buffer) {
148 | message = buffer.readString(Short.MAX_VALUE);
149 | }
150 |
151 | public SendPack(String message) {
152 | this.message = message;
153 | }
154 |
155 | public void toBytes(PacketBuffer buf) {
156 | buf.writeString(this.message);
157 | }
158 |
159 | public void handler(Supplier ctx) {
160 | ctx.get().enqueueWork(() -> {
161 | LOGGER.info(this.message);
162 | });
163 | ctx.get().setPacketHandled(true);
164 | }
165 | }
166 | ```
167 |
168 | 首先`toBytes`方法和`SendPack(PacketBuffer buffer)`构造方法刚好是一对相反的方法,它们的作用我们之前已经提及,这里就不加赘述了。值得一说的是`PacketBuffer`下面提供了很多非常方便的方法来序列化基本类型,以及在有多个变量序列化时,你得保证这两个方法中调用这些变量的顺序是相同的。请注意查看一下你调用的方法是不是物理客户端独有的(也就是有没有加`@OnlyIn(Dist.CLIENT)`注释)。
169 |
170 | 然后就是`handler`方法,这个方法的作用就是在接收端接收到数据以后,如何使用这些数据。请注意,你必须把这里的执行操作放在`ctx.get().enqueueWork`这个方法内,以闭包的形式呈现。并且在执行完成后需要加上`ctx.get().setPacketHandled(true);`表示执行成功。之所以这么做,是因为接受网络数据处在一个独立的线程中,所以网络包的执行需要等待时机,在线程安全的情况下执行。在这里我们只是简单的创建了一个Logger然后调用这个Logger输出了内容。
171 |
172 | 最后还剩下的构造方法是是用来构建发送数据用的。
173 |
174 | 当然光创建了Channel和网络数据包还不够,我们在游戏启动时创建它。
175 |
176 | `CommonEventHandler.java`:
177 |
178 | ```java
179 | @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
180 | public class CommonEventHandler {
181 | @SubscribeEvent
182 | public static void onCommonSetup(FMLCommonSetupEvent event) {
183 | Networking.registerMessage();
184 | }
185 | }
186 | ```
187 |
188 | 你需要在`Mod`总线中的`FMLCommonSetupEvent`这个生命周期方法中创建你的Channel。
189 |
190 | 接下来看一个实例,如何从客户端向服务端发送数据,以及从服务端向客户端发送数据。
191 |
192 | `ObsidianMessage`:
193 |
194 | ```java
195 | public class ObsidianMessage extends Item {
196 | public ObsidianMessage() {
197 | super(new Properties().group(ModGroup.itemGroup));
198 | }
199 |
200 | @Override
201 | public ActionResult onItemRightClick(World worldIn, PlayerEntity playerIn, Hand handIn) {
202 | if (worldIn.isRemote) {
203 | Networking.INSTANCE.sendToServer(new SendPack("From the Client"));
204 | }
205 | if (!worldIn.isRemote) {
206 | Networking.INSTANCE.send(
207 | PacketDistributor.PLAYER.with(
208 | () -> {
209 | return (ServerPlayerEntity) playerIn;
210 | }
211 | ),
212 | new SendPack("From Server"));
213 | }
214 | return super.onItemRightClick(worldIn, playerIn, handIn);
215 | }
216 | }
217 | ```
218 |
219 | 可以看到这里重要的是`onItemRightClick`方法。
220 |
221 | 在这个方法里,主要分成了服务端和客户端两个逻辑。
222 |
223 | 在服务端逻辑里非常简单我们通过`Networking.INSTANCE.sendToServer`方法向服务端发送了数据,其中`new SendPack("From the Client”)`就是数据包的具体内容。
224 |
225 | 在服务端里稍微有些复杂,因为一个服务端有可能有多个客户端链接,所以你必须要明确你需要向哪一个客户端发送消息。
226 |
227 | ```java
228 | PacketDistributor.PLAYER.with(
229 | () -> {
230 | return (ServerPlayerEntity) playerIn;
231 | }
232 | )
233 | ```
234 |
235 | 作用正是这个,我们确定了要向右击了这个物品的玩家发送消息。`PacketDistributor`类下除了有`PLAYER`这个类型还有很多其他的发送数据的方式,大家可自行探索。
236 |
237 | 打开游戏,右击物品,你应该就能看见客户端和服务端接收到对方的消息了。
238 |
239 | 
240 |
241 | 注:「Server Thread」是服务端,「Render Thread」是客户端。
242 |
243 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/network)
244 |
245 | ## 编程小课堂
246 |
247 | 在任何开始接触一个你不熟悉的编程项目,你要做的第一件事永远是通读这个项目的文档(如果有的话),对于我们来说就是[Forge的文档](http://mcforge.readthedocs.io/en/latest/),介于读者可能看不懂英文文档,Forge文档也有中文翻译的,请自行搜索。
--------------------------------------------------------------------------------
/src/gui/firstgui.md:
--------------------------------------------------------------------------------
1 | # 第一个Gui
2 |
3 | 在这一节中,我们将创建一个自定义的Gui,在开始教程之前,我必须要强调一点,打开Gui等操作只能在客户端执行,不能在服务端执行。
4 |
5 | 与GUI相关的最直接的一个类就是`Screen`类,自然我们要创建一个类并继承`Screen`。
6 |
7 | `ObsidianFirstGui.java`:
8 |
9 | ```java
10 | public class ObsidianFirstGui extends Screen {
11 | TextFieldWidget textFieldWidget;
12 | Button button;
13 | OptionSlider optionSlider;
14 | ResourceLocation OBSIDIAN_FIRST_GUI_TEXTURE = new ResourceLocation("neutrino", "textures/gui/first_gui.png");
15 | String content = "Hello";
16 | SliderPercentageOption sliderPercentageOption;
17 | Widget sliderBar;
18 |
19 | protected ObsidianFirstGui(ITextComponent titleIn) {
20 | super(titleIn);
21 | }
22 |
23 | @Override
24 | protected void init() {
25 | this.minecraft.keyboardListener.enableRepeatEvents(true);
26 | this.textFieldWidget = new TextFieldWidget(this.font, this.width / 2 - 100, 66, 200, 20, "Context");
27 | this.children.add(this.textFieldWidget);
28 |
29 | this.button = new Button(this.width / 2 - 40, 96, 80, 20, "Save", (button) -> {
30 | });
31 | this.addButton(button);
32 |
33 | this.sliderPercentageOption = new SliderPercentageOption("neutrino.sliderbar", 5, 100, 5, (setting) -> {
34 | return Double.valueOf(0);
35 | }, (setting, value) -> {
36 | }, (gameSettings, sliderPercentageOption1) -> "test");
37 | this.sliderBar = this.sliderPercentageOption.createWidget(Minecraft.getInstance().gameSettings, this.width / 2 - 100, 120, 200);
38 | this.children.add(this.sliderBar);
39 |
40 | super.init();
41 | }
42 |
43 | @Override
44 | public void render(int mouseX, int mouseY, float particleTick) {
45 | this.renderBackground();
46 | RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
47 | this.minecraft.getTextureManager().bindTexture(OBSIDIAN_FIRST_GUI_TEXTURE);
48 | int textureWidth = 208;
49 | int textureHeight = 156;
50 | this.blit(this.width / 2 - 150, 10, 0, 0, 300, 200, textureWidth, textureHeight);
51 | this.drawString(this.font, content, this.width / 2 - 10, 30, 0xeb0505);
52 |
53 | this.textFieldWidget.render(mouseX, mouseY, particleTick);
54 | this.button.render(mouseX, mouseY, particleTick);
55 | this.sliderBar.render(mouseX, mouseY, particleTick);
56 | super.render(mouseX, mouseY, particleTick);
57 | }
58 | }
59 | ```
60 |
61 | 在这里最重要的是两个方法,`init`和`render`方法。首先,我们先来讲`init`方法。
62 |
63 | ```java
64 | @Override
65 | protected void init() {
66 | this.minecraft.keyboardListener.enableRepeatEvents(true);
67 | this.textFieldWidget = new TextFieldWidget(this.font, this.width / 2 - 100, 66, 200, 20, "Context");
68 | this.children.add(this.textFieldWidget);
69 |
70 | this.button = new Button(this.width / 2 - 40, 96, 80, 20, "Save", (button) -> {
71 | });
72 | this.addButton(button);
73 |
74 | this.sliderPercentageOption = new SliderPercentageOption("neutrino.sliderbar", 5, 100, 5, (setting) -> {
75 | return Double.valueOf(0);
76 | }, (setting, value) -> {
77 | }, (gameSettings, sliderPercentageOption1) -> "test");
78 | this.sliderBar = this.sliderPercentageOption.createWidget(Minecraft.getInstance().gameSettings, this.width / 2 - 100, 120, 200);
79 | this.children.add(this.sliderBar);
80 | super.init();
81 | }
82 | ```
83 |
84 | 在这里我们创建了三个「Widget(组件)」——Button(按钮)、TextFieldWidget(文本框)以及Slider(滑条)。Widget 是Minecraft GUI中最小可交互的对象。在GUI中添加Widget大体上可以分成两步骤:
85 |
86 | 1. 创建:你需要先创建一个组件,在创建组件时你需要指定它的宽和高、X坐标和Y坐标,对于一个特殊的组件,你还得指定它的回调函数,也就是当你操作组件后它需要执行的内容。比如说,当你按下按钮,你需要执行的内容就是一个回调函数。
87 | 2. 添加,为了你的GUI可以使用组件,你需要在创建完组件之后将组件添加到GUI上,对于绝大部分的组件你只需要调用`this.children.add(组件实例)`即可,按钮比较特殊,你需要调用`this.addButton(按钮实例)`。
88 |
89 | 接下来我们来讲解一下窗口布局,对于我们的这个Screen类来说,你可以通过`this.width`和`this.height`来获取宽度和高度。请注意,在GUI中,X轴是从左上角向下,Y轴是从左上角向右。
90 |
91 | 因为这里涉及到的方法很多,我就稍微讲解一下,大体上所有的组件创建的都有X、Y位置设定,以及宽和高的设定,大家可以自己看方法签名。
92 |
93 | 其中值得一讲的是:
94 |
95 | ```java
96 | this.button = new Button(this.width / 2 - 40, 96, 80, 20, "Save", (button) -> {});
97 | ```
98 |
99 | 这里最后一个参数,这个空的闭包就是按钮的回调函数,如果你希望你的按钮能做什么的话,就在这个闭包内写上逻辑吧。
100 |
101 | ```java
102 | this.sliderPercentageOption = new SliderPercentageOption("neutrino.sliderbar", 5, 100, 5, (setting) -> {
103 | return Double.valueOf(0);
104 | }, (setting, value) -> {
105 | }, (gameSettings, sliderPercentageOption1) -> "test");
106 | ```
107 |
108 | 接下来是滚动条,它比较特殊,你得先创建一个`SliderPercentageOption`,然后调用它的`createWidget`的组件,之所以这么做是因为滚动条必须要和一个数据范围相对应。
109 |
110 | 这里的第五和第六个参数是`getter`和`setter`,这个是用来设置滚动条相对应的数值用的,你可在这里写上非常复杂的逻辑。当你调用它相对应的`set`和`get`方法时,会先执行你设置的这两个方法,然后获取值。这里我们不进行任何的设置,返回值也设置成0,最后一个方法是用来设置滚动条底部文字的,因为要实现滚动条处于不同地方时呈现不同的内容,所以这里也是一个闭包(虽然我们没有用到这个功能),其他参数的意义请自行查看函数签名。
111 |
112 | 接下来就`render`方法。
113 |
114 | ```java
115 | @Override
116 | public void render(int mouseX, int mouseY, float particleTick) {
117 | this.renderBackground();
118 | RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
119 | this.minecraft.getTextureManager().bindTexture(OBSIDIAN_FIRST_GUI_TEXTURE);
120 | int textureWidth = 208;
121 | int textureHeight = 156;
122 | this.blit(this.width / 2 - 150, 10, 0, 0, 300, 200, textureWidth, textureHeight);
123 | this.drawString(this.font, content, this.width / 2 - 10, 30, 0xeb0505);
124 |
125 | this.textFieldWidget.render(mouseX, mouseY, particleTick);
126 | this.button.render(mouseX, mouseY, particleTick);
127 | this.sliderBar.render(mouseX, mouseY, particleTick);
128 | super.render(mouseX, mouseY, particleTick);
129 | }
130 | ```
131 |
132 | 我们将在这里渲染背景图片。
133 |
134 | 首先我们调用了` RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F)`来确保我们渲染出来的图片是正常的,这里的三个值分别代表着RGBA,红绿蓝和透明度。你电脑上所有能看到的颜色都是这4个元素的混合,这里的值规定了颜色的范围,如果RGB三个值不相等会出现偏色的现象,如果小于1会出现某些颜色无法显示,整个画面会变暗变灰。具体的效果大家可以自行调式使用。
135 |
136 | 你需要用`this.minecraft.getTextureManager().bindTexture(OBSIDIAN_FIRST_GUI_TEXTURE);`绑定你需要渲染的图片,这里用`ResouceLocation`指明了你得图片在资源包中的位置,在我们的例子里是:
137 |
138 | ```java
139 | ResourceLocation OBSIDIAN_FIRST_GUI_TEXTURE = new ResourceLocation("neutrino", "textures/gui/first_gui.png");
140 | ```
141 |
142 | 其中第一个参数请填入你的`ModId`,后面的内容就是具体的位置。
143 |
144 | 然后调用blit方法来正式渲染。
145 |
146 | Blit方法的参数还没有被指定,你可以查看这个[gigaherz](https://gist.github.com/gigaherz)提供的[文件](https://gist.github.com/gigaherz/f61fe604f38e27afad4d1553bc6cf311)来获取翻译好的函数签名。
147 |
148 | 我们用的函数签名如下。
149 |
150 | ```java
151 | blit(int x0, int y0, int z, float u0, float v0, int width, int height, int textureHeight, int textureWidth)
152 | ```
153 |
154 | 
155 |
156 | 这几个参数的作用如上图。
157 |
158 | 其中没有讲到的`U`和`V`(相当于是XY,用UV是计算机图形学的一个传统)是用来指定你背景图片在实际图片中的左上角位置的。之所以要这么做,是因为对于GPU来说切换图片是一个非常耗时的工作,所以如果可能的话,你应该把所以要用的的元素放在同一张图片中,然后通过指定不同的UV,来指定它的位置。
159 |
160 | 当你的`textureHeight`和`textureWidth`小于`width`和`height`时,渲染结果如下。
161 |
162 | 
163 |
164 | 当你的`textureHeight`和`textureWidth`大于`width`和`height`时,渲染结果如下。
165 |
166 | 
167 |
168 | 然后我们调用如下方法,绘制了文字。
169 |
170 | ```java
171 | this.drawString(this.font, content, this.width / 2 - 10, 30, 0xeb0505);
172 | ```
173 |
174 | 然后调用如下方法绘制了我们的组件:
175 |
176 | ```java
177 | this.textFieldWidget.render(mouseX, mouseY, particleTick);
178 | this.button.render(mouseX, mouseY, particleTick);
179 | this.sliderBar.render(mouseX, mouseY, particleTick);
180 | ```
181 |
182 | 至于打开一个只存在在客户端的GUI也很简单,`ObsidianFirstGuiItem`:
183 |
184 | ```java
185 | public class ObsidianFirstGuiItem extends Item {
186 | public ObsidianFirstGuiItem() {
187 | super(new Properties().group(ModGroup.itemGroup));
188 | }
189 |
190 |
191 | @Override
192 | public ActionResult onItemRightClick(World worldIn, PlayerEntity playerIn, Hand handIn) {
193 | if (worldIn.isRemote) {
194 | DistExecutor.runWhenOn(Dist.CLIENT, () -> () -> {
195 | OpenGuI.openGUI();
196 | });
197 | }
198 | return super.onItemRightClick(worldIn, playerIn, handIn);
199 | }
200 | }
201 |
202 | ```
203 |
204 | 你只需要先判断是不是在客户端,然后调用一个额外的类来打开GUI,`DistExecutor.runWhenOn`这个函数的第一个参数是用来判断物理端的,因为物理服务器上是没有`Screen`的,所以我们不能在这个类里触发类加载(因为Item类在物理客户端上有),我们得到另一个类里触发类加载,第一参数`Dist.CLIENT`就是用来指定物理端的,第二类参数是一个两层的lambda表达式,在这里面我们调用了`OpenGuI.openGUI()`类来打开gui,`DistExecutor`下有很多用来判断物理端而进行不同操作的函数,大家可以按需选用。
205 |
206 | ```java
207 | public class OpenGuI {
208 | public static void openGUI() {
209 | Minecraft.getInstance().displayGuiScreen(new ObsidianFirstGui(new StringTextComponent("test")));
210 | }
211 | }
212 | ```
213 |
214 | 因为在之前已经通过`DistExecutor.runWhenOn`来判断过物理端了,所以在`OpenGuI`类中,我们不用担心`Screen`缺失的问题,我们在这里调用`Minecraft.getInstance().displayGuiScreen`方法显示GUI,第二个参数是你GUI的标题,我们这里没有渲染标题,但是还是需要填入一个。
215 |
216 | 打开游戏你就可以看见我们的GUI被渲染出来了。
217 |
218 | 
219 |
220 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/first_gui)
221 |
222 | ## 编程小课堂
223 |
224 | 在Mod开发中请不要使用`Time.sleep`来计时,这种行为非常非常的愚蠢,如果你需要计时,请使用游戏内置的Tick。
--------------------------------------------------------------------------------
/src/worldgeneration/dimensionenerationandchunkgeneratorandbiomeprovider.md:
--------------------------------------------------------------------------------
1 | # 自定义维度与区块生成器以及生物群系提供器
2 |
3 | 首先先原谅我起了这么长的一个标题。在这节中,我们将要来学习如何生成一个维度,在这个过程中我们还会学习到如何写区块生成器以及生物群系提供器。
4 |
5 | 在开始我们的代码之前,我们得先来理一下维度生成的过程是什么样子的。
6 |
7 | 首先我们有一个`Dimension`,他规定了维度的高度、天空的颜色等。在`Dimension`中有一个`ChunkGenerator`,它负责根据特定的算法决定了维度的地形。在`ChunkGenerator`里有个`BiomeProvider`,它决定了特定地方是什么生物群系。然后Minecraft会根据生物群系添加类似于生物,结构等东西。
8 |
9 | 知道了这个过程我们就可以来创建我们的维度了。Forge在这个过程之上还提供了一个叫做`ModDimension`来自动的帮我们做了类似于数据保存与恢复,客户端和服务端创建维度时发包等操作。所以我们首先需要新建一个自己的`ModDimension`。
10 |
11 | ```java
12 | public class ObsidianModDimensions extends ModDimension {
13 | @Override
14 | public BiFunction getFactory() {
15 | return (world, type) -> {
16 | return new ObsidianDimension(world, type);
17 | };
18 | }
19 | }
20 | ```
21 |
22 | 可以看到这里的内容非常简单,只是返回了一个我们自己的维度而已。
23 |
24 | 接下来来看维度的具体内容。
25 |
26 | ```java
27 | public class ObsidianDimension extends Dimension {
28 | public ObsidianDimension(World world, DimensionType dimensionType) {
29 | super(world, dimensionType, 0f);
30 | }
31 |
32 |
33 | @Override
34 | public ChunkGenerator> createChunkGenerator() {
35 | return new ObsidianChunkGenerator(world, new ObsidianBiomeProvider());
36 | }
37 |
38 | @Nullable
39 | @Override
40 | public BlockPos findSpawn(ChunkPos chunkPosIn, boolean checkValid) {
41 | return null;
42 | }
43 |
44 | @Nullable
45 | @Override
46 | public BlockPos findSpawn(int posX, int posZ, boolean checkValid) {
47 | return null;
48 | }
49 |
50 | @Override
51 | public int getActualHeight() {
52 | return 256;
53 | }
54 |
55 | @Override
56 | public float calculateCelestialAngle(long worldTime, float partialTicks) {
57 | int j = 6000;
58 | float f1 = (j + partialTicks) / 24000.0F - 0.25F;
59 |
60 | if (f1 < 0.0F) {
61 | f1 += 1.0F;
62 | }
63 |
64 | if (f1 > 1.0F) {
65 | f1 -= 1.0F;
66 | }
67 |
68 | float f2 = f1;
69 | f1 = 1.0F - (float) ((Math.cos(f1 * Math.PI) + 1.0D) / 2.0D);
70 | f1 = f2 + (f1 - f2) / 3.0F;
71 | return f1;
72 | }
73 |
74 | @Override
75 | public boolean isSurfaceWorld() {
76 | return true;
77 | }
78 |
79 | @Override
80 | public boolean hasSkyLight() {
81 | return true;
82 | }
83 |
84 | @Override
85 | public Vec3d getFogColor(float celestialAngle, float partialTicks) {
86 | return new Vec3d(0, 0, 0);
87 | }
88 |
89 | @Override
90 | public boolean canRespawnHere() {
91 | return false;
92 | }
93 |
94 | @Override
95 | public boolean doesXZShowFog(int x, int z) {
96 | return false;
97 | }
98 |
99 | }
100 | ```
101 |
102 | 我们首先来看维度的构造方法,
103 |
104 | ```java
105 | public ObsidianDimension(World world, DimensionType dimensionType) {
106 | super(world, dimensionType, 0f);
107 | }
108 | ```
109 |
110 | 这里的`0F`具体变量名我还不能确定,应该是类似于`particalTick`之类的,这里填入0就行。
111 |
112 | 其他除了`createChunkGenerator`方法之外的所有方法都是用来配置维度的属性。稍微值得一提的是`calculateCelestialAngle`,这是用来计算星体在天空中的角度用的。
113 |
114 | ```java
115 | @Override
116 | public ChunkGenerator> createChunkGenerator() {
117 | return new ObsidianChunkGenerator(world, new ObsidianBiomeProvider());
118 | }
119 | ```
120 |
121 | 我们在`createChunkGenerator`创建并返回了我们自定义的`ChunkGenerator`,并且给它了一个自定义的`BiomeProvider`,接下来我们来看看,这两个是如何实现的。
122 |
123 | 首先是我们自定义的`ChunkGenerator`
124 |
125 | ```java
126 | public class ObsidianChunkGenerator extends ChunkGenerator {
127 | public ObsidianChunkGenerator(IWorld world, BiomeProvider provider) {
128 | super(world, provider, createDefault());
129 | }
130 | public static GenerationSettings createDefault() {
131 | GenerationSettings config = new GenerationSettings();
132 | config.setDefaultBlock(Blocks.DIAMOND_BLOCK.getDefaultState());
133 | config.setDefaultFluid(Blocks.LAVA.getDefaultState());
134 | return config;
135 | }
136 |
137 | @Override
138 | public void generateSurface(WorldGenRegion region, IChunk chunk) {
139 | BlockState bedrock = Blocks.BEDROCK.getDefaultState();
140 | BlockState stone = Blocks.STONE.getDefaultState();
141 | ChunkPos chunkpos = chunk.getPos();
142 |
143 | BlockPos.Mutable pos = new BlockPos.Mutable();
144 |
145 | int x;
146 | int z;
147 |
148 | for (x = 0; x < 16; x++) {
149 | for (z = 0; z < 16; z++) {
150 | chunk.setBlockState(pos.setPos(x, 0, z), bedrock, false);
151 | }
152 | }
153 |
154 | for (x = 0; x < 16; x++) {
155 | for (z = 0; z < 16; z++) {
156 | int realx = chunkpos.x * 16 + x;
157 | int realz = chunkpos.z * 16 + z;
158 | int height = (int) (65 + Math.sin(realx / 20.0f) * 10 + Math.cos(realz / 20.0f) * 10);
159 | for (int y = 1; y < height; y++) {
160 | chunk.setBlockState(pos.setPos(x, y, z), stone, false);
161 | }
162 | }
163 | }
164 | }
165 |
166 | @Override
167 | public int getGroundHeight() {
168 | return world.getSeaLevel() + 1;
169 | }
170 |
171 | @Override
172 | public void makeBase(IWorld worldIn, IChunk chunkIn) {
173 |
174 | }
175 |
176 | @Override
177 | public int func_222529_a(int p_222529_1_, int p_222529_2_, Heightmap.Type heightmapType) {
178 | return 0;
179 | }
180 | }
181 | ```
182 |
183 | 我们的类继承了`ChunkGenerator`这里的`GenerationSettings`说明了我们的需要一个`GenerationSettings`。这里我们直接继承了`ChunkGenerator`,并且自己实现了地形生成算法,如果你想要原版的地形生成算法,你可以选择继承`NoiseChunkGenerator`。
184 |
185 | 我们来看构造方法。
186 |
187 | ```java
188 | public ObsidianChunkGenerator(IWorld world, BiomeProvider provider) {
189 | super(world, provider, createDefault());
190 | }
191 | public static GenerationSettings createDefault() {
192 | GenerationSettings config = new GenerationSettings();
193 | config.setDefaultBlock(Blocks.DIAMOND_BLOCK.getDefaultState());
194 | config.setDefaultFluid(Blocks.LAVA.getDefaultState());
195 | return config;
196 | }
197 | ```
198 |
199 | 可以看到,我们直接调用`createDefault`创建了一个默认的`GenerationSettings`,然后设置了默认的方块和流体。这是一个最为简单的`GenerationSettings`了,如果你想要对你的维度进行更加复杂的配置,你可以选择继承并创建一个你自己的`GenerationSettings`。
200 |
201 | 接下来我们在`generateSurface`写了一个简单的地形生成算法,无非就是按照Sin函数周期的生成地形而已。其他的函数保持默认。
202 |
203 | 接下来是我们的`BiomeProvider`
204 |
205 | ```java
206 | public class ObsidianBiomeProvider extends BiomeProvider {
207 | private static final List BIOMES = new ArrayList<>(Arrays.asList(Biomes.PLAINS, Biomes.OCEAN));
208 | private Random random;
209 |
210 | protected ObsidianBiomeProvider() {
211 | super(new HashSet<>(BIOMES));
212 | random = new Random();
213 | }
214 |
215 | @Override
216 | public Biome getNoiseBiome(int x, int y, int z) {
217 | return BIOMES.get(random.nextInt(2));
218 | }
219 | }
220 | ```
221 |
222 | 这里的内容也非常简单,无非是声明这个维度有的生物群系而已。
223 |
224 | ```java
225 | @Override
226 | public Biome getNoiseBiome(int x, int y, int z) {
227 | return BIOMES.get(random.nextInt(2));
228 | }
229 | ```
230 |
231 | 这个是用来给指定位置返回生物群系用的,这里我们就随机从两个声明的生物群系中返回了一个。
232 |
233 | 这样我们的类就创建完毕了,接下来就是注册。
234 |
235 | 首先你得注册`ModDimensions`。
236 |
237 | ```java
238 | public class ModDimensionRegistry {
239 | public static final DeferredRegister MOD_DIMENSION = new DeferredRegister<>(ForgeRegistries.MOD_DIMENSIONS, "neutrino");
240 | public static RegistryObject obsidianModDimension = MOD_DIMENSION.register("obsidian_mod_dimension", () -> {
241 | return new ObsidianModDimensions();
242 | });
243 | }
244 | ```
245 |
246 | 同样的,我我们用了`DeferredRegister`,就不多说了,别忘了将`MOD_DIMENSION` 在你的Mod主类里添加到Mod总线上。
247 |
248 | 接下是是注册我们的`Dimensions`:
249 |
250 | ```java
251 | @Mod.EventBusSubscriber
252 | public class DimensionsEventHandler {
253 | public static final ResourceLocation DIMENSION_ID = new ResourceLocation("neutrino", "obsidian");
254 | public static DimensionType DIMENSION_TYPE;
255 |
256 | @SubscribeEvent
257 | public static void onDimensionsRegistry(RegisterDimensionsEvent event) {
258 | if (DimensionType.byName(DIMENSION_ID) == null) {
259 | DIMENSION_TYPE = DimensionManager.registerDimension(DIMENSION_ID, ModDimensionRegistry.obsidianModDimension.get(), null, true);
260 | }
261 | }
262 | }
263 | ```
264 |
265 | 可以看见我们监听了Forge总线上的`RegisterDimensionsEvent`事件,然后调用`DimensionManager.registerDimension`注册注册了我们的维度。这里的if语句是为了避免我们重复注册维度导致游戏崩溃。
266 |
267 | 这里的`DIMENSION_ID`代表的是你`Dimension`的名字请保证是唯一的,`DIMENSION_TYPE`之后如果你需要可以用来获取你注册的`Dimension`,这里我们不会用到它,当还是保留作为演示。
268 |
269 | 至此我们的维度就创建完毕了。
270 |
271 | 创建一个世界,输入Forge提供的切换维度的命令。
272 |
273 | ```
274 | /forge setdimension Dev neurino:obsdian
275 | ```
276 |
277 | 就可以进入我们创建的维度了。
278 |
279 | 
280 |
281 | 因为我们没有设置天空颜色之类的,所以看上很奇怪,但是这就是我们的维度了。
282 |
283 | [源代码](https://github.com/FledgeXu/NeutrinoSourceCode/tree/master/src/main/java/com/tutorial/neutrino/dimensions)
--------------------------------------------------------------------------------