巷ではインクを塗り合って陣地を取り合うゲームが流行っているらしい。あいにく私はそのゲームができるハードを持っていない。とりあえずマイクラ上でできるようにMODを開発することにした。
方針
今回は地面に色を塗れるブキ武器の実装のみに留めて、ゲーム要素は今後気が向いたら実装していくことにする。
まずは近距離の塗りと攻撃ができるブキ武器「シューター」を実装してみる。以下のようなシステムを考えた。
シューターをメインハンドに持って右クリック→インクを模したエンティティを発射→エンティティに当たればダメージ、地面に当たれば色を塗る
この方針では、ブキ武器そのものの他にインクを模したエンティティを実装する必要がある。
EntityInkの実装
インクを模したエンティティをEntityInkとして実装した。コードを以下に示す。今回は分かりやすいようにダイヤモンドブロックに置き換えている。
public class EntityInk extends Snowball {
    private static final float DAMAGE = 5.0f; // プレイヤーは4発で確定キル
    private static ArrayList<Block> exceptBlocks = new ArrayList<>();
    public EntityInk(EntityType<? extends Snowball> entityTypeIn, Level level) {
        super(entityTypeIn, level);
    }
    public EntityInk(Level level, LivingEntity throwerIn) {
        super(level, throwerIn);
        // 除外するブロック
        exceptBlocks.add(Blocks.GLASS);
        exceptBlocks.add(Blocks.GLASS_PANE);
        exceptBlocks.add(Blocks.WATER);
        exceptBlocks.add(Blocks.LAVA);
        exceptBlocks.addAll(BlockTags.LEAVES.getValues());
        exceptBlocks.addAll(BlockTags.DOORS.getValues());
    }
    
    @Override
    protected void onHit(HitResult result) {
        super.onHit(result);
        // 地面に当たったときは色を塗る
        if (result.getType() == HitResult.Type.BLOCK) {
            BlockPos pos = new BlockPos(result.getLocation());   
            boolean isexceptBlock = false;
            // posが空気ブロックではなく、かつposの1ブロック上が空気ブロックであるとき
            if (!level.getBlockState(pos).isAir() && 
                    level.getBlockState(pos.offset(0,1,0)).isAir()) {
                
                // 着弾地点が除外するブロックであったとき
                for (Block block : exceptBlocks) {
                    if (level.getBlockState(pos).is(block)) {
                        isexceptBlock = true;
                        break;
                    }
                }
                if (!isexceptBlock) {
                    /* 着弾地点が除外するブロックではなく、かつ1ブロック上が空気ブロックであれば、
                       着弾地点は必ず塗る */
                    level.setBlockAndUpdate(pos, Blocks.DIAMOND_BLOCK.defaultBlockState());
                }
            }
            for (int i = -1; i < 2; i++) {
                for (int j = -1; j < 2; j++) {
                    // 3x3の領域のうち、地表のみを塗る
                    int dy = 0;
                    for (; level.getBlockState(pos.offset(i,dy,j)).isAir(); dy--) {
                    }  
                    BlockPos gndPos = pos.offset(i,dy,j);
                    // 地表が除外するブロックであったとき
                    for (Block block : exceptBlocks) {
                        if (level.getBlockState(gndPos).is(block)) {
                            isexceptBlock = true;
                            break;
                        }
                    }
                    /* インクが飛び散る様子を再現したい
                       boolean型の乱数がtrueになったときのみ塗る */
                    Random random = new Random();
                    boolean isPainting = random.nextBoolean();
                    /* 除外するブロックではなく、かつ乱数がtrueで、
                       かつ1ブロック上が空気ブロックであるとき
                       (着弾地点の1ブロック上が空気ブロックであるが、
                        周囲8ブロックの1ブロック上が空気ブロックではないこともあり得る) */
                    if (!isexceptBlock && isPainting &&
                        level.getBlockState(gndPos.offset(0,1,0)).isAir())
                    {
                        level.setBlockAndUpdate(gndPos, Blocks.DIAMOND_BLOCK.defaultBlockState());
                    }
                }
            }
        // エンティティに当たったときはダメージを与える
        } else if (result.getType() == HitResult.Type.ENTITY) {      
            EntityHitResult entityRayTraceResult = (EntityHitResult) result;
            entityRayTraceResult.getEntity().hurt(DamageSource.thrown(this, getOwner()), DAMAGE);
        }
    }
} 
着弾地点が地面であれば、着弾地点を中心に3x3のブロックを塗る。ベタ塗りしても面白くないので、周囲8ブロックは塗るか否かをランダムにしてインクが飛び散る様子を再現した。苦労した点は地面の判定である。空気ブロックかそうでないかをbooleanで返してくれるisAirメソッドがあったのでそれを利用した。同様にbooleanを返してくれるisメソッドを利用してガラスや葉は塗られないようにした。ゲームシステムを実装していくときステージ作成に役立てたい。
ItemShooterの実装
右クリックするとEntityInkを発射できるアイテムをItemShooterとして実装した.
コードを以下に示す。
public class ItemShooter extends Item {
    public ItemShooter() {
        super(new Item.Properties().tab(CreativeModeTab.TAB_COMBAT));
    }
    @Override
    // 右クリックしたときの実装
    public InteractionResultHolder<ItemStack> use(Level level, Player playerIn, InteractionHand handIn) {
        Random random = new Random();
        // 弓の発射音を鳴らす
        level.playSound(
                null,
                playerIn.getX(),
                playerIn.getY(),
                playerIn.getZ(),
                SoundEvents.ARROW_SHOOT,
                SoundSource.NEUTRAL,
                0.5f,
                0.4f / (random.nextFloat() * 0.4f + 0.8f)
        );
        if (!level.isClientSide()) {
            // EntityInkを発射
            EntityInk entity = new EntityInk(level, playerIn);
            /* 第2引数は発射角度、やや下向きに
               第5引数は速度、矢よりも速め
               第6引数は不正確性、エイムを定まりにくく */
            entity.shootFromRotation(playerIn, playerIn.xRotO + 15.0f, playerIn.yRotO, 0.0f, 3.0f, 5.0f);
            level.addFreshEntity(entity);
        }
        return super.use(level, playerIn, handIn);
    }
}
動かす
アイテムとエンティティを登録し、テクスチャを反映させ、起動してみる。
 
GIFアニメーションにつきガビガビになっていが、クリックして再生してみて頂きたい。
下を向いていると足元しか塗られないのでなるべく正面を向くようにしよう。
柵がダイヤモンドブロックに化けてしまっているが、今後柵は塗られずに柵の直下のブロックが塗られるように改良するつもりである。
終わりに
いかがだったであろうか。今後は気が向けばゲームとして成立するようにシステム面を実装していきたいし、新たなブキ武器も実装したい。イカしたMODになるように改良していきたい。
