2025年1月24日金曜日

Minecraft の forge MOD でブロックイベントをいろいろと試してみる

Minecraft の forge MOD でブロックイベントをいろいろと試してみる

概要

今回は以下のイベントを使ってMOD用のブロックをいろいろと操作してみます

  • ブロックが破壊されたときのイベント
  • ユーザによって右クリックでブロックが設置されたときのイベント
  • エンティティが設置されたときのイベント

環境

  • macOS 15.2
  • Java 21.0.5
  • forrge MDK 1.20.6-50.1.32
  • minecraft 1.20.6

BlockEvent.BreakEvent

BlockEvent.BreakEvent はブロックが破壊されたときにコールされるイベントハンドラです
イベントからブロック状況など様々な情報を取得することができます
今回はクリエイティブモード中でかつMODブロックが破壊されたときにその破壊をキャンセルするという処理をしています

これ以外にもいろいろなアクションを実装することができます

forge MOD でイベントハンドリングする場合には EventBusSubscriber でハンドリングするクラスにデコレータし更に各イベントごとに SubscribeEvent デコレータを使ってハンドラメソッドを定義すると簡単です

@Mod.EventBusSubscriber(modid = MODID)
public class BlockEventHandler {

    @SubscribeEvent
    public static void onBlockBreak(BlockEvent.BreakEvent event) {
        Block brokenBlock = event.getState().getBlock();
        // 例えば、MODで作成したブロックの破壊をキャンセルする
        if (event.getPlayer().isCreative() || brokenBlock == EXAMPLE_BLOCK.get()) {
            LOGGER.info("クリエイティブモード中に{}のブロックは削除されません", MODID);
            event.setCanceled(true); // 破壊をキャンセル
        }
    }
}

BlockEvent.EntityPlaceEvent

BlockEvent.EntityPlaceEvent はブロックなどのエンティティが配置されたときにコールされるイベントハンドラです

今度は少し複雑な処理をしていてMODブロックが設置された際にその隣接するブロックを調査しMODブロックだった場合には双方のMODブロックを破壊するという処理にしています

先程の破壊のイベントと組み合わせると「一度設置すると破壊できないが同じブロックを隣接させると破壊できるブロック」がこれで実装できます

またワールドを扱うハンドラの場合には bus = Mod.EventBusSubscriber.Bus.FORGE という感じで指定します
これはワールド全体でイベントを監視するときなどに使います

@Mod.EventBusSubscriber(modid = MODID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public class BlockPlacementHandler {
    @SubscribeEvent
    public static void onBlockPlace(BlockEvent.EntityPlaceEvent event) {
        Level world = event.getEntity().level();
        if (world.isClientSide()) {
            return; // サーバー側でのみ処理
        }
        // 設置されたブロック情報を取得
        BlockState placedBlockState = event.getPlacedBlock();
        BlockPos placedPos = event.getPos();
        // MODブロックかどうかを判定
        if (!(placedBlockState.getBlock() == EXAMPLE_BLOCK.get())) {
            return; // MODブロック以外は無視
        }
        // 周囲のMODブロックを探す
        for (Direction direction : Direction.values()) {
            BlockPos neighborPos = placedPos.relative(direction);
            BlockState neighborState = world.getBlockState(neighborPos);
            if (neighborState.getBlock() == EXAMPLE_BLOCK.get()) {
                // 隣接するMODブロックが見つかった場合、両方を破壊
                LOGGER.info("MODブロック同士が連結されました、破壊します");
                destroyBlock(world, placedPos);
                destroyBlock(world, neighborPos);
                return; // 処理終了
            }
        }
    }

    private static void destroyBlock(Level world, BlockPos pos) {
        BlockState blockState = world.getBlockState(pos);
        if (!blockState.isAir()) { // 空気ブロックは無視
            world.destroyBlock(pos, true); // ブロックを破壊し、ドロップを生成
        }
    }
}

PlayerInteractEvent.RightClickBlock

PlayerInteractEvent.RightClickBlock はユーザが右クリックした際にコールされるイベントハンドラです
先ほどまでとは少し観点を変えておりユーザのアクションに応じてブロックを操作するようにしています

実行している処理は同じで隣接したらMODブロックを削除するようにしています
ただ一番の違いは右クリックのイベントを扱うハンドラなのでまだ新規のブロックはワールドにない判定になっています
なので破壊されるブロックは元々おいてあった隣接ブロックだけになり新規で設置したブロックは破壊できません

またイベントからワールドの情報などを取得する経路もブロックのイベントとは少し違っています

BlockEvent.EntityPlaceEvent も PlayerInteractEvent.RightClickBlock もイベント的にはどちらも同時に扱えますが今回の場合はどちらも隣接ブロックを破壊する処理になっているので実際はどちらか一方のイベントハンドラだけ使えば OK です

@Mod.EventBusSubscriber(modid = MODID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public class BlockPlacementHandler {
    @SubscribeEvent
    public static void onPlayerInteract(PlayerInteractEvent.RightClickBlock event) {
        Level world = event.getLevel();
        // クライアント側のイベントを無視
        if (world.isClientSide()) {
            return;
        }
        LOGGER.info("プレイヤーが右クリックでブロックを設置しました");
        Player player = event.getEntity();
        ItemStack heldItem = player.getMainHandItem();
        // プレイヤーが持っているアイテムがブロックでなければ無視
        if (!(heldItem.getItem() instanceof BlockItem)) {
            return;
        }
        // アイテムからブロックを取得
        Block blockToPlace = ((BlockItem) heldItem.getItem()).getBlock();
        // MODのブロックでなければ無視
        if (!(blockToPlace == EXAMPLE_BLOCK.get())) {
            return;
        }
        // プレイヤーが右クリックした位置
        BlockPos clickedPos = event.getPos();
        // 右クリックしたブロックの隣接位置を計算 (右クリック位置の隣にブロックが置かれる)
        BlockPos placementPos = clickedPos.relative(event.getFace());
        // 周囲のブロックを確認
        for (Direction direction : Direction.values()) {
            BlockPos neighborPos = placementPos.relative(direction); // 隣接位置
            BlockState neighborState = world.getBlockState(neighborPos); // 隣接ブロックの状態
            Block neighborBlock = neighborState.getBlock();
            // 隣接ブロックがMODのブロックなら結合とみなす
            if (neighborBlock == EXAMPLE_BLOCK.get()) {
                LOGGER.info("MODブロック同士が連結されました、破壊します");
                // 隣接するMODブロックが見つかった場合、連結とみなして片方を破壊
                // destroyBlock(world, placementPos); // RightClickBlock
                // で設置されたブロックはまだワールドに存在しない判定なので新規で設置したブロックは破壊できない
                destroyBlock(world, neighborPos);
                return;
            }
        }
    }

    private static void destroyBlock(Level world, BlockPos pos) {
        BlockState blockState = world.getBlockState(pos);
        if (!blockState.isAir()) { // 空気ブロックは無視
            world.destroyBlock(pos, true); // ブロックを破壊し、ドロップを生成
        }
    }
}

コード全体

  • vim src/main/java/com/example/examplemod/ExampleMod.java
package com.example.examplemod;

import com.mojang.logging.LogUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.food.FoodProperties;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.CreativeModeTabs;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.MapColor;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.BuildCreativeModeTabContentsEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.event.level.BlockEvent;
import net.minecraftforge.event.server.ServerStartingEvent;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;
import org.slf4j.Logger;

// The value here should match an entry in the META-INF/mods.toml file
@Mod(ExampleMod.MODID)
public class ExampleMod {
    // Define mod id in a common place for everything to reference
    public static final String MODID = "examplemod";
    // Directly reference a slf4j logger
    private static final Logger LOGGER = LogUtils.getLogger();
    // Create a Deferred Register to hold Blocks which will all be registered under the "examplemod"
    // namespace
    public static final DeferredRegister<Block> BLOCKS =
            DeferredRegister.create(ForgeRegistries.BLOCKS, MODID);
    // Create a Deferred Register to hold Items which will all be registered under the "examplemod"
    // namespace
    public static final DeferredRegister<Item> ITEMS =
            DeferredRegister.create(ForgeRegistries.ITEMS, MODID);
    // Create a Deferred Register to hold CreativeModeTabs which will all be registered under the
    // "examplemod" namespace
    public static final DeferredRegister<CreativeModeTab> CREATIVE_MODE_TABS =
            DeferredRegister.create(Registries.CREATIVE_MODE_TAB, MODID);

    // Creates a new Block with the id "examplemod:example_block", combining the namespace and path
    public static final RegistryObject<Block> EXAMPLE_BLOCK =
            BLOCKS.register(
                    "example_block",
                    () -> new Block(BlockBehaviour.Properties.of().mapColor(MapColor.STONE)));
    // Creates a new BlockItem with the id "examplemod:example_block", combining the namespace and
    // path
    public static final RegistryObject<Item> EXAMPLE_BLOCK_ITEM =
            ITEMS.register(
                    "example_block",
                    () -> new BlockItem(EXAMPLE_BLOCK.get(), new Item.Properties()));

    // Creates a new food item with the id "examplemod:example_id", nutrition 1 and saturation 2
    public static final RegistryObject<Item> EXAMPLE_ITEM =
            ITEMS.register(
                    "example_item",
                    () ->
                            new Item(
                                    new Item.Properties()
                                            .food(
                                                    new FoodProperties.Builder()
                                                            .alwaysEdible()
                                                            .nutrition(1)
                                                            .saturationModifier(2f)
                                                            .build())));

    // Creates a creative tab with the id "examplemod:example_tab" for the example item, that is
    // placed after the combat tab
    public static final RegistryObject<CreativeModeTab> EXAMPLE_TAB =
            CREATIVE_MODE_TABS.register(
                    "example_tab",
                    () ->
                            CreativeModeTab.builder()
                                    .withTabsBefore(CreativeModeTabs.COMBAT)
                                    .icon(() -> EXAMPLE_ITEM.get().getDefaultInstance())
                                    .displayItems(
                                            (parameters, output) -> {
                                                output.accept(
                                                        EXAMPLE_ITEM
                                                                .get()); // Add the example item to
                                                // the tab. For your own
                                                // tabs, this method is
                                                // preferred over the event
                                            })
                                    .build());

    public ExampleMod() {
        IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();

        // Register the commonSetup method for modloading
        modEventBus.addListener(this::commonSetup);

        // Register the Deferred Register to the mod event bus so blocks get registered
        BLOCKS.register(modEventBus);
        // Register the Deferred Register to the mod event bus so items get registered
        ITEMS.register(modEventBus);
        // Register the Deferred Register to the mod event bus so tabs get registered
        CREATIVE_MODE_TABS.register(modEventBus);

        // Register ourselves for server and other game events we are interested in
        MinecraftForge.EVENT_BUS.register(this);

        // Register the item to a creative tab
        modEventBus.addListener(this::addCreative);

        // Register our mod's ForgeConfigSpec so that Forge can create and load the config file for
        // us
        ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, Config.SPEC);
    }

    private void commonSetup(final FMLCommonSetupEvent event) {
        // Some common setup code
        LOGGER.info("HELLO FROM COMMON SETUP");

        if (Config.logDirtBlock)
            LOGGER.info("DIRT BLOCK >> {}", ForgeRegistries.BLOCKS.getKey(Blocks.DIRT));

        LOGGER.info(Config.magicNumberIntroduction + Config.magicNumber);

        Config.items.forEach((item) -> LOGGER.info("ITEM >> {}", item.toString()));
    }

    // Add the example block item to the building blocks tab
    private void addCreative(BuildCreativeModeTabContentsEvent event) {
        if (event.getTabKey() == CreativeModeTabs.BUILDING_BLOCKS) event.accept(EXAMPLE_BLOCK_ITEM);
    }

    // You can use SubscribeEvent and let the Event Bus discover methods to call
    @SubscribeEvent
    public void onServerStarting(ServerStartingEvent event) {
        // Do something when the server starts
        LOGGER.info("HELLO from server starting");
    }

    // You can use EventBusSubscriber to automatically register all static methods in the class
    // annotated with @SubscribeEvent
    @Mod.EventBusSubscriber(
            modid = MODID,
            bus = Mod.EventBusSubscriber.Bus.MOD, // このイベントバスはデフォルトの値
            value = Dist.CLIENT)
    public static class ClientModEvents {
        @SubscribeEvent
        public static void onClientSetup(FMLClientSetupEvent event) {
            // Some client setup code
            LOGGER.info("HELLO FROM CLIENT SETUP");
            LOGGER.info("MINECRAFT NAME >> {}", Minecraft.getInstance().getUser().getName());
        }
    }

    @Mod.EventBusSubscriber(modid = MODID)
    public class BlockEventHandler {

        @SubscribeEvent
        public static void onBlockBreak(BlockEvent.BreakEvent event) {
            Block brokenBlock = event.getState().getBlock();
            // 例えば、MODで作成したブロックの破壊をキャンセルする
            if (event.getPlayer().isCreative() || brokenBlock == EXAMPLE_BLOCK.get()) {
                LOGGER.info("クリエイティブモード中に{}のブロックは削除されません", MODID);
                event.setCanceled(true); // 破壊をキャンセル
            }
        }
    }

    @Mod.EventBusSubscriber(modid = MODID, bus = Mod.EventBusSubscriber.Bus.FORGE)
    public class BlockPlacementHandler {
        @SubscribeEvent
        public static void onBlockPlace(BlockEvent.EntityPlaceEvent event) {
            Level world = event.getEntity().level();
            if (world.isClientSide()) {
                return; // サーバー側でのみ処理
            }
            // 設置されたブロック情報を取得
            BlockState placedBlockState = event.getPlacedBlock();
            BlockPos placedPos = event.getPos();
            // MODブロックかどうかを判定
            if (!(placedBlockState.getBlock() == EXAMPLE_BLOCK.get())) {
                return; // MODブロック以外は無視
            }
            // 周囲のMODブロックを探す
            for (Direction direction : Direction.values()) {
                BlockPos neighborPos = placedPos.relative(direction);
                BlockState neighborState = world.getBlockState(neighborPos);
                if (neighborState.getBlock() == EXAMPLE_BLOCK.get()) {
                    // 隣接するMODブロックが見つかった場合、両方を破壊
                    LOGGER.info("MODブロック同士が連結されました、破壊します");
                    destroyBlock(world, placedPos);
                    destroyBlock(world, neighborPos);
                    return; // 処理終了
                }
            }
        }

        @SubscribeEvent
        public static void onPlayerInteract(PlayerInteractEvent.RightClickBlock event) {
            Level world = event.getLevel();
            // クライアント側のイベントを無視
            if (world.isClientSide()) {
                return;
            }
            LOGGER.info("プレイヤーが右クリックでブロックを設置しました");
            Player player = event.getEntity();
            ItemStack heldItem = player.getMainHandItem();
            // プレイヤーが持っているアイテムがブロックでなければ無視
            if (!(heldItem.getItem() instanceof BlockItem)) {
                return;
            }
            // アイテムからブロックを取得
            Block blockToPlace = ((BlockItem) heldItem.getItem()).getBlock();
            // MODのブロックでなければ無視
            if (!(blockToPlace == EXAMPLE_BLOCK.get())) {
                return;
            }
            // プレイヤーが右クリックした位置
            BlockPos clickedPos = event.getPos();
            // 右クリックしたブロックの隣接位置を計算 (右クリック位置の隣にブロックが置かれる)
            BlockPos placementPos = clickedPos.relative(event.getFace());
            // 周囲のブロックを確認
            for (Direction direction : Direction.values()) {
                BlockPos neighborPos = placementPos.relative(direction); // 隣接位置
                BlockState neighborState = world.getBlockState(neighborPos); // 隣接ブロックの状態
                Block neighborBlock = neighborState.getBlock();
                // 隣接ブロックがMODのブロックなら結合とみなす
                if (neighborBlock == EXAMPLE_BLOCK.get()) {
                    LOGGER.info("MODブロック同士が連結されました、破壊します");
                    // 隣接するMODブロックが見つかった場合、連結とみなして片方を破壊
                    // destroyBlock(world, placementPos); // RightClickBlock
                    // で設置されたブロックはまだワールドに存在しない判定なので新規で設置したブロックは破壊できない
                    destroyBlock(world, neighborPos);
                    return;
                }
            }
        }

        private static void destroyBlock(Level world, BlockPos pos) {
            BlockState blockState = world.getBlockState(pos);
            if (!blockState.isAir()) { // 空気ブロックは無視
                world.destroyBlock(pos, true); // ブロックを破壊し、ドロップを生成
            }
        }
    }
}

最後に

forge MOD でブロックのイベントハンドリングを試してみました
まだまだいろいろなイベントがあるので興味があれば試してみてください

Block クラスを継承してカスタムブロックを定義した場合に同じように onRemove や onPlace といったメソッドを使って同じような挙動を実装できます
今回のようにメインの ExampleMod 側でイベントハンドラを使ってブロックの挙動を実装した場合との違いは以下です

特性 Block#onRemove BlockEvent.BreakEvent
呼び出し元 Block クラス内 Forge イベントシステム
スコープ 自分自身のブロックのみ ゲーム全体のブロック破壊イベント
処理の細かさ 特定ブロックの動作に最適 全体を監視し、条件分岐で制御可能
オーバーヘッド 軽量で直接呼び出される イベントシステムを介して間接的に呼ばれる
他モッドとの連携 他モッドの影響を受けない 他モッドと競合や依存関係が発生し得る

参考サイト

0 件のコメント:

コメントを投稿