2025年1月30日木曜日

forge MOD で攻撃時に音声を再生する方法

forge MOD で攻撃時に音声を再生する方法

概要

前回レーザーガンっぽい武器を作成しました
今回はレーザ発射時に音声を再生するようにしてみたいと思います

環境

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

サウンドファイルの配置

1秒程度の ogg 形式のファイルを準備しましょう
レーザっぽい音のフリー素材を見つけてくる感じで OK です

  • mkdir -p src/main/resources/assets/examplemod/sounds/laser_fire.ogg

sounds.json の作成

設置した音声リソースを利用する設定を記載します

  • vim src/main/resources/assets/examplemod/sounds.json
{
  "laser_fire": {
    "sounds": [
      {
        "name": "examplemod:laser_fire",
        "volume": 1.0,
        "pitch": 1.0
      }
    ]
  }
}

SoundEvent の登録

Mod 自体に追加した音を登録する必要があります
アイテム登録などと同じような感じで登録します

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

主要部分は以下です

import net.minecraft.sounds.SoundEvent;
import net.minecraft.resources.ResourceLocation;

// サウンドイベントの作成
public static final DeferredRegister<SoundEvent> SOUND_EVENTS =
        DeferredRegister.create(ForgeRegistries.SOUND_EVENTS, "examplemod");

// レーザガンサウンドの登録
public static final RegistryObject<SoundEvent> LASER_FIRE_SOUND =
        SOUND_EVENTS.register(
                "laser_fire",
                () ->
                        SoundEvent.createVariableRangeEvent(
                                new ResourceLocation("examplemod", "laser_fire")));

public ExampleMod() {
    // 他の登録と同様に DeferredRegister を登録
    SOUND_EVENTS.register(modEventBus);
}

Mod メイン全体は以下のようにしています
今回と関係ない部分も多いです

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.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvent;
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.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.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);
    // サウンドイベントの作成
    public static final DeferredRegister<SoundEvent> SOUND_EVENTS =
            DeferredRegister.create(ForgeRegistries.SOUND_EVENTS, "examplemod");

    // 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)));
    // カスタムブロックの登録
    public static final RegistryObject<Block> CUSTOM_BLOCK =
            BLOCKS.register("custom_block", CustomBlock::new);

    // 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()));
    // レーザガンアイテムの登録
    public static final RegistryObject<Item> LASER_GUN_ITEM =
            ITEMS.register("laser_gun", () -> new LaserGunItem(new Item.Properties().stacksTo(1)));
    // カスタムブロックアイテムの登録
    public static final RegistryObject<Item> CUSTOM_BLOCK_ITEM =
            ITEMS.register(
                    "custom_block", () -> new BlockItem(CUSTOM_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())));
    // レーザガンサウンドの登録
    public static final RegistryObject<SoundEvent> LASER_FIRE_SOUND =
            SOUND_EVENTS.register(
                    "laser_fire",
                    () ->
                            SoundEvent.createVariableRangeEvent(
                                    new ResourceLocation("examplemod", "laser_fire")));

    // 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);
        // 他の登録と同様に DeferredRegister を登録
        SOUND_EVENTS.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);
        // レーザガンをCombatタブに追加
        if (event.getTabKey() == CreativeModeTabs.COMBAT) event.accept(LASER_GUN_ITEM);
        // カスタムブロックを建築ブロックタブに追加
        if (event.getTabKey() == CreativeModeTabs.BUILDING_BLOCKS) event.accept(CUSTOM_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());
        }
    }

    // EXAMPLE_BLOCK が破壊されない制御
    // ワールド全体で制御したい場合はMODメインのイベントハンドラで制御するがMODだけで制御したい場合はCustomBlock.javaのようにカスタムクラス側のイベントハンドラで制御するのが望ましい
    @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; // 処理終了
                }
            }
        }

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

右クリックレーザ発射時に音を再生するコードを追加

前回作成した LaserGunItem クラスに音を再生するコードを追加します
Mod メイン側に追加したサウンドイベントから音源を取得し playSound で再生します
音源の再生だけなのでクライアント側の処理かと思いきやサウンドイベント自体が Mod 側のリソースなのでサーバ側で音声を再生するようです

  • vim src/main/java/com/example/examplemod/LaserGunItem.java

主要部分は fireLaser メソッドの以下部分です

// 音の再生
if (world instanceof ServerLevel serverLevel) {
    serverLevel.playSound(
            null, // プレイヤー(nullにすると全員が聞こえる)
            player.blockPosition(), // 再生位置
            ExampleMod.LASER_FIRE_SOUND.get(), // サウンドイベント
            SoundSource.PLAYERS, // サウンドの種類(PLAYERS, BLOCKS, AMBIENT など)
            1.0f, // 音量
            1.0f // 音の高さ(ピッチ)
            );

LaserGunItem.java 全体のコードは以下の通りです

package com.example.examplemod;

import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.projectile.ProjectileUtil;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;

public class LaserGunItem extends Item {
    private float amountOfdamage = 100.0F; // ダメージ量

    public LaserGunItem(Properties properties) {
        super(properties);
    }

    @Override
    public InteractionResultHolder<ItemStack> use(
            Level world, Player player, InteractionHand hand) {
        // クライアント側のみでエフェクトを表示
        if (world.isClientSide()) {
            // ビームエフェクトを生成
            spawnLaserBeam(player);
        } else {
            // サーバー側でダメージ処理
            fireLaser(world, player);
        }
        return InteractionResultHolder.sidedSuccess(
                player.getItemInHand(hand), world.isClientSide());
    }

    @Override
    public void appendHoverText(
            ItemStack stack,
            Item.TooltipContext context,
            List<Component> tooltip,
            TooltipFlag flag) {
        super.appendHoverText(stack, context, tooltip, flag);
        // ダメージ情報を追加
        tooltip.add(
                Component.translatable("item.examplemod.laser_gun.damage", amountOfdamage)
                        .withStyle(ChatFormatting.GREEN));

        // 他の情報も追加可能
        tooltip.add(
                Component.translatable("item.examplemod.laser_gun.description")
                        .withStyle(ChatFormatting.GRAY));
    }

    @Override
    public boolean onEntityItemUpdate(ItemStack stack, ItemEntity entity) {
        // 特殊な処理が必要であればここで行う(今回は不要なのでデフォルト動作に任せます)
        // アイテムが q で捨てられたときにアイテムとして残すための設定
        return super.onEntityItemUpdate(stack, entity);
    }

    private void spawnLaserBeam(Player player) {
        // クライアント側エフェクト(例:パーティクル)
        player.level()
                .addParticle(
                        ParticleTypes.END_ROD,
                        player.getX(),
                        player.getEyeY(),
                        player.getZ(),
                        player.getLookAngle().x,
                        player.getLookAngle().y,
                        player.getLookAngle().z);
    }

    private void fireLaser(Level world, Player player) {
        // 音の再生
        if (world instanceof ServerLevel serverLevel) {
            serverLevel.playSound(
                    null, // プレイヤー(nullにすると全員が聞こえる)
                    player.blockPosition(), // 再生位置
                    ExampleMod.LASER_FIRE_SOUND.get(), // サウンドイベント
                    SoundSource.PLAYERS, // サウンドの種類(PLAYERS, BLOCKS, AMBIENT など)
                    1.0f, // 音量
                    1.0f // 音の高さ(ピッチ)
                    );
        }
        // 発射用の座標と距離の設定
        Vec3 start = player.getEyePosition(1.0F);
        Vec3 direction = player.getLookAngle();
        double range = 50.0D; // 50ブロック先をターゲット
        Vec3 end = start.add(direction.scale(range));

        // ブロックとのヒット判定
        HitResult blockHit =
                world.clip(
                        new ClipContext(
                                start,
                                end,
                                ClipContext.Block.OUTLINE,
                                ClipContext.Fluid.NONE,
                                player));
        // エンティティとのヒット判定
        EntityHitResult entityHit =
                ProjectileUtil.getEntityHitResult(
                        world,
                        player,
                        start,
                        end,
                        player.getBoundingBox().expandTowards(direction.scale(range)).inflate(1.0D),
                        entity -> entity instanceof LivingEntity && entity != player);
        // 優先的にエンティティを処理
        if (entityHit != null) {
            System.out.println("Hit entity!");
            Entity hitEntity = entityHit.getEntity();
            if (hitEntity instanceof LivingEntity livingEntity) {
                // 敵にダメージを与える
                livingEntity.hurt(
                        livingEntity.damageSources().playerAttack(player), amountOfdamage);
            }
        } else if (blockHit != null && blockHit.getType() == HitResult.Type.BLOCK) {
            // ブロックにヒットした場合
            System.out.println("Hit block at: " + ((BlockHitResult) blockHit).getBlockPos());
            BlockHitResult blockResult = (BlockHitResult) blockHit;
            BlockPos hitPos = blockResult.getBlockPos();
            // ヒット位置に火をつける例
            world.setBlockAndUpdate(hitPos, Blocks.FIRE.defaultBlockState());
        } else {
            // 何にも当たらなかった場合
            System.out.println("Missed!");
        }
    }
}

動作確認

  • ./gradlew clean && ./gradlew build && ./gradlew runClient

レーザ照射時に追加したサウンドが鳴ることを確認しましょう

最後に

forge Mod で追加したアイテムを使用したした際に効果音を再生する方法を紹介しました
音声形式は ogg ファイルのみサポートしているようなので注意しましょう

参考サイト

0 件のコメント:

コメントを投稿