A Fabric module for widget-based GUI for Minecraft
jamcgui presents a clearer control of GUI rendering and handling using three main components:
- Screen Renderer - extended from
HandledScreen
, a client-side instance for storing widgets to be drawn - Widget - a listening object to handle user-to-handler interactions
- Screen Renderer Handler - extended from
ScreenHandler
, a context handler between the client and the server
As a result, the vanilla HandledScreen
is no longer in-charge
of event listening. If you want to build a GUI that interacts
with the gameplay, this module can be beneficial for you!
jamcgui also offers a utility file to perform networking between the client and the server.
Add this path to your mod dependency
dependencies {
modImplementation "io.github.ye-yu:jamcgui:0.0.1-alpha.4"
// uncomment below to bundle jar
// include "io.github.ye-yu:jamcgui:0.0.1-alpha.4"
}
dependencies {
modImplementation("io.github.ye-yu:jamcgui:0.0.1-alpha.4")
// uncomment below to bundle jar
// include("io.github.ye-yu:jamcgui:0.0.1-alpha.4")
}
This mod is written in Kotlin (because kotlin is life uwu). Read about Fabric Kotlin here. If you don't bundle this mod, you can obtain jamcgui latest release at the release page.
You must extend the ScreenRenderer
and
ScreenRendererHandler
class based on your requirement, and then
you have to register ScreenRendererHandler
on the server-side
and ScreenRenderer
on the client-side. Next, you add the
relevant widgets in the screen renderer. Finally, you
spawn the GUI on the relevant player action.
-
Extending & Registering
ScreenRendererHandler
You need to understand that a screen handler instance exists on both client and server side. You can create two screen-handler classes each for the server and the client, extending a common screen handler class. This can be useful for screen networking. The server screen handler must implements
ServerScreenHandlerPacketListener
while the client implementsClientScreenHandlerPacketListener
. Otherwise, if you want to make both sides to instantiate the screen handler of the same class, you implement both interfaces on the same handler class (vanilla does this). More networking information is in the networking section.Registering your new screen handler occurs in the fabric 'main' mod initializer class. The registered reference must be accessible on the 'client' mod initializer too as a reference to register the screen renderer. In the factory parameter, the passed lambda expression must return the client screen handler given that you create separate screen handlers for the client and the server (otherwise, just return the same handler instance for server screen handler). The returned value is an instance of
ScreenHandlerType<your common super handler for client & server>
public static ScreenHandlerType<ScreenRendererHandler> MY_CUSTOM_SCREEN_HANDLER; ... // in ModInitializer MY_CUSTOM_SCREEN_HANDLER = ScreenHandlerRegistry .registerSimple(<Identifier>, (int syncId, PlayerInventory inventory) -> { return new ClientScreenHandler(MY_CUSTOM_SCREEN_HANDLER, ...) /* or below if don't create separate handler client & server */ return new ClientAndServerScreenHandler(MY_CUSTOM_SCREEN_HANDLER, ...) } );
Related reference:
-
Extending & Registering
ScreenRenderer
Extend
ScreenRenderer
and overrideinit()
method. In this method, you put in the relevant parent widgets and listeners into the renderer instance. Parent widgets are panels, etc, and they take in child widgets. For this reason, screen renderer is not responsible for drawing child widgets, and we delegate this take to parent widgets instead. So, screen renderer must take in parent widget instance throughaddParent
method.Widgets that listen to client event must be added separately using
addListener
regardless of widget type (parent or child). For example, text field widget listens to key input, so remember to add this to the screen renderer listeners. Label widgets are not a kind of listener, so they should not be added to the listeners.When you are finally done with the widgets, you register the screen renderer into the client mod initializer. Pass in the screen handler type registered in the 'main' mod initializer in the previous step.
ScreenRegistry.<ScreenRendererHandler, BlockScreenRenderer>register( MY_CUSTOM_SCREEN_HANDLER, (screenRendererHandler, playerInventory, text) -> new BlockScreenRenderer(screenRendererHandler, playerInventory, text) );
Related reference:
-
Spawning GUI
You can spawn the GUI through various actions such as on block use, on entity interactions, etc. For convenience, the entity must contain a singleton of
NamedScreenHandlerFactory
, which can be implemented in any class. In the class implementation, the implemented methodcreateMenu
must return the corresponding server screen handler instance.@Override public ScreenHandler createMenu(int syncId, PlayerInventory inv, PlayerEntity player) { return new ServerScreenHandler(MY_CUSTOM_SCREEN_HANDLER, syncId, inv); /* or below if don't create separate handler client & server */ return new ClientAndServerScreenHandler(MY_CUSTOM_SCREEN_HANDLER, ...) }
To spawn the GUI, you pass the singleton into
ServerPlayerEntity#openHandledScreen
method.private final NamedScreenHandlerFactory customGuiFactory = <some implemented class> ... @Override public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hitResult) { player.openHandledScreen(customGuiFactory); return ActionResult.SUCCESS; }
Related reference:
Networking can be useful for GUI that manipulates gameplay. The server relies on the client packets to drive the gameplay, and the client also relies on packets to render the gameplay on the screen.
The first thing to remember in networking is handshakes. You can't send packets to client if client has not initialised a GUI. The package provided a utility file ScreenPacket for a manageable packet communication. In this example, we are going to send an 'init' packet to the server to tell that the client can now receive screen-related packets.
To send an 'init' packet to the server, the client
can use the method ScreenPacket#sendPacket
. The
buffer wrapper parameter is a consumer lambda
to append any extra information to the server
screen handler. Since this packet is for the server,
the player parameter can be left null.
ScreenPacket.INSTANCE.sendPacket(syncId, "init", true, null, it -> {
it.writeString("I have inited");
return null;
});
On the server screen handler, the packet
must be handled in onClient2Server
method
public void onClient2Server(String action, PacketContext context, PacketByteBuf buf) {
if (action.equalsIgnoreCases("init")) {
PlayerEntity player = context.getPlayer();
String msg = buf.readString();
System.out.println(player.getEntityName() + " has inited their GUI. They says: " + msg);
}
}
'init' packet can be useful to start sending properties that is not easily accessible on the client-side such as entity inventories. The client does not have access to the server entity instance, so it cannot access the entity inventories as soon as the client open an entity GUI. Since the entity instance resides on the server-side, the server can read the entity inventories information and broadcast this information to the client. However, if the server sends the packets before the client gets to open the entity GUI, the packet will be lost. That is why 'init' packet is crucial.
There are several other ways packets can be used to manipulate gameplay. Take a look at the example from the appended related reference.
Related reference:
There are more pre-implemented widgets and screen handlers
that could be useful for most cases. If you are creating
widgets, ChildWidget
and ParentWidget
are the available
options. Widget listens to client events but can query
handler data too. Implement provider interfaces
on the client(!) handler, and cast handler type to the implemented
provider interface to run the interface method. Handler
but return the appropriate value based on the passed "name"
value.
While listening to events, widget can broadcast update to the client handler too. Implement handler listener interfaces on the client(!) handler. The client handler usually have to send the received value to the server handler so that the server can compute the correct action.
As a rule of thumb, implement provider if you are only conveying and drawing information from the server to the client. Implement listener if you want to send client events to the server.
!: Must be a client handler
A wiki will be added for future public reference.