"左"クリック検知の現状

この記事は、[Minecraft Command Advent Calendar 2023](https://qiita.com/advent-calendar/2023/mccmd) 23日目の記事です。:santa:

# はじめに

「○○したらコマンドを実行する」といったトリガーの中で、右クリック... ではなく、**左クリック**をきっかけにコマンドを実行したいと思ったことはありますね。
この記事ではそんな、かれこれ3年ほど試行錯誤して完璧になった左クリックの検知を解説しよう！！！

......... と思ったのですが、仕様やバグのせいで未だに完璧な検知が難しい左クリックの現状と苦悩を書き殴っていこうと思います。既に試行済の手法や、現状分かっている不具合とその対策を可能な限り記して、以降同じようなことをする人の助けになることを目的としています。

:::note warn
Java版 Minecraft ver 1.20.2 時点の情報となります。
:::

# 左クリック検知の仕組み

マイクラの中でプレイヤーが左クリックするタイミングといえば、キー割り当て画面からもわかる通り「攻撃をする／ブロックを壊そうとする」の2つのパターンが存在しますが、

![leftclick.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3589374/4191ec64-0701-b5cb-cd16-e40fdbe28b87.png)
今回は攻撃したことの検知をして一瞬の左クリックを検知していきます。

## player_hurt_entity

**攻撃したこと**はadvancementの条件のうちの一つ、player_hurt_entityを用いることで検知が可能です。また、Conditionで攻撃対象のエンティティに条件を付けることで、**特定のタグがついたエンティティに攻撃したこと**を検知できます。

``` json:advent:<font color="#C3CAD5">player_hurt_entity</font>
{
    "criteria": {
        "requirement": {
            "trigger": "minecraft:player_hurt_entity",
            "conditions": {
                "entity": {
                    "nbt": "{Tags:[\"LeftClick\"]}"
                }
            }
        }
    },
    "rewards": {
        "function": "advent:leftclick/detected"
    }
}
```

報酬のfunctionにおいては、適当な確認用のコマンドとadvancementを剥奪するコマンドを置いておきます。


``` mcfunction:<font color="#C3CAD5">advent:leftclick/detected</font>
title @s title "左クリックした！"
advancement revoke @s only advent:player_hurt_entity
```

ここまでで、特定のタグを持ったエンティティがプレイヤーのリーチ内にいて、かつそのモブを向いている状態での左クリック検知が完成します。

![leftclick1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3589374/5647a048-e329-6075-2471-61918dd08391.gif)

## 左クリック検知用のエンティティを召喚

とはいえ、ここまでではあくまでもモブがそこにいる場合の左クリック検知 (ただの攻撃検知) ができたに過ぎず、当然そこにモブがいなければ左クリックの検知はできません。

![leftclick2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3589374/fca67c38-09b2-1bdc-55e1-c9d96f9d1965.gif)

今回は右クリック検知のような、**そこに何もなくても左クリックでコマンドを実行できる (ようにみえる) もの**が作りたいわけで...。まずは、左クリック検知用の適当なエンティティをプレイヤーの目前に召喚していきます。

### 召喚するモブは「ヴェックス」
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3589374/c48616ad-f6f0-d7be-348f-c987997e818e.gif">

左クリック検知用のエンティティには元々スライムやパンダを使っていたのですが、最近は**ヴェックス**を使う方針で固まっています。

> ヴェックスは空気中を飛行することができ、水を含むあらゆる固体ブロックを透過することができる。ヴェックスが攻撃するときは赤く光り、突っ込んでくる。また、炎や溶岩の影響を受けない。- https://minecraft.fandom.com/ja/wiki/ヴェックス



水や炎、溶岩などの自然の影響を受けることがなく、更には固体ブロックを透過することによって窒息ダメージの影響 ([後述](#検知用のモブにダメージを与えられない)) も受けないため、左クリック検知にうってつけです。

また、検知用ヴェックスの召喚の際にはプレイヤーとの衝突判定をなくすためのteamを設定することや、HPを多く設定するなどしておきます。

``` mcfunction:<font color="#C3CAD5">準備 (衝突判定を無くすためのteam)</font>
team add NoCollision
team modify NoCollision collisionRule never
```

召喚タイミングについては常時実行のfunctionにおいて、検知用ヴェックスをkillした**あと**、新しい検知用ヴェックスをsummonします。こうすることで、常に1体の検知用ヴェックスをプレイヤーの目前に用意することができます。

``` mcfunction:<font color="#C3CAD5">advent:leftclick/summon</font>
tp @e[type=vex,tag=LeftClick] ~ -1000 ~
execute as @a at @s anchored eyes positioned ^ ^ ^ run summon vex ~ ~-0.6 ~ {Silent:1b,NoAI:1b,Health:1024f,Team:"NoCollision",Tags:["LeftClick"],Attributes:[{Name:"generic.max_health",Base:1024}]}
```

![leftclick3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3589374/4b8fd1ff-8187-0263-79e6-060d85a30776.gif)
<font color="#888888">常に目前に召喚されるので、左クリックが必ず検知用モブへの攻撃になる！</font>

ちなみに、透明エフェクトは召喚時1tick目には反映されないため、現状私は目前にまとわりつくヴェックスの見た目を消すために<font color="#D01919">ヴェックスのテクスチャそのものをリソースパックで除いています。 </font>ヴェックスは尊い犠牲となったのだ。

... と、ここまでが基本の流れです。

# 左クリック検知の問題点

左クリック検知は右クリック検知などの直接的な検知とは違い、**召喚されてはkillされる、目前に召喚されるモブを攻撃したことを検知**という複雑な実装をせざるを得ないがゆえ、沢山の問題を抱えています。試行錯誤によって対策できる場合もありましたが、未だ解決していないものも存在しています。

## <font color="#D01919">検知用のモブにダメージを与えられない</font>

検知用のモブが壁に埋まり窒息ダメージを受けることで、0.5秒間の無敵時間 (HurtTime) が始まり、**素手での攻撃のダメージが無効化されてしまいます**。ダメージが与えられないとplayer_hurt_entityをトリガーできないため、左クリック検知が機能しなくなります。

### 対策①: 検知用モブにヴェックスを用いる

[前述](#召喚するモブはヴェックス)の通り、ヴェックスはそもそも自然の影響や壁に埋まってダメージを受けることがないため、ダメージを与えられない状況が生まれることがありません。

![leftclick9.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3589374/622c1320-9dc2-231b-2865-376856b87569.gif)

### 対策②: 窒息ダメージより高いダメージを与える

> 一度ダメージを受けたプレイヤーや生物は、その後0.5秒間、直前に受けたダメージを上回るダメージ以外を無効化する(無敵時間[immunity period])。直前に受けたダメージを上回るダメージを受けた場合には差の分だけ有効となるが、これによって無敵時間は延長されない。- https://minecraftjapan.miraheze.org/wiki/ダメージ

無敵時間の仕様のおかげで、たとえ検知用のモブが窒息ダメージを受けた直後であっても、プレイヤーの攻撃によるダメージが窒息ダメージの1.0を少しでも上回れば、左クリック検知ができるようになります。<font color="#D01919">攻撃を十分にチャージしないとダメージが攻撃力以下の威力となってしまうことや、プレイヤー死亡時に攻撃力のattributeが基準値1.0にリセットされてしまうこと</font>には注意が必要です。

``` mcfunction:<font color="#C3CAD5">プレイヤーの素の攻撃力を1.0→10.0にする</font>
attribute @s minecraft:generic.attack_damage base set 10.0
```

## <font color="#D01919">右クリックで作用 (Interact) できなくなる</font>

チェストや木のドアなど、普段ゲームプレイにおいて右クリックで作用できるものが、目前の検知用モブによって作用できなくなります。更に、**Interactionエンティティを用いた場合に至っては、弓や消費アイテムが使用できなくなります**。

### 対策①: 作用可能ブロックが視点先にある場合はモブを召喚しない

「木のドアが視点先にある場合は検知用モブを召喚しない」ように実装することで、視点先に作用可能なブロックが検知できた際には目前に検知用モブが召喚されず、通常通り右クリックで木のドアを開けることが叶います。

![leftclick5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3589374/d437cc0a-2129-3198-928e-7e70af6b71be.gif)

ただ、<font color="#D01919">今度は木のドア前での左クリック検知ができなくなります。</font>こういったどうしようもない場合では現状、断腸の思いでどちらかを妥協する必要があります。渋い...。

### 対策②: 右クリック検知→ブロックへの作用を再現する

右クリックが検知出来た際、視点先のドアの座標を割り出し、向きを考慮して元のドアを置き換えるようにドアを設置、ドアを開けたときの音を/playsoundで...。言葉では簡単に言えますが、一つ一つ再現していくのは気が遠くなりそうです。

## <font color="#D01919">特定の条件下で、検知用モブへの攻撃が外れる</font>

>プレイヤーのX, Z座標: $X_A, Z_A$
>適当なエンティティのX, Z座標: $X_B$,$Z_B$

**プレイヤーと適当なエンティティがいるチャンクが異なり、$X_A<X_B$｜$Z_A<Z_B$ となる場合に限り**、攻撃が目前の検知用モブではなく、チャンクを跨いだ向こう側にいるエンティティに当たります。視点を合わせているエンティティ (Targeted Entity) も向こう側のエンティティと判定されてしまうので、マイクラ内部の判定の順番周辺の仕様／バグだと考えられます。

![leftclick6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3589374/66d7c81d-0c8c-d2a1-8056-94a814109ea5.gif)
<font color="#888888">目前のヴェックスに攻撃するはずが、判定をすり抜けて村人を攻撃してしまう</font>

### 対策: プレイヤーを左クリック検知用のモブで囲う

検知用のヴェックスを1体→6体に増やし、プレイヤーを囲うように配置します。

![leftclick7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3589374/c174267d-48bc-39d3-a0e3-cc0056f83185.gif)
<font color="#888888">チャンクを跨いでも、判定をすり抜けることなく左クリックを検知できている！</font>

``` mcfunction:<font color="#C3CAD5">advent:leftclick/summon</font>
tp @e[type=vex,tag=LeftClick] ~ -1000 ~
execute as @a at @s run summon vex ~ ~1.625 ~ {Silent:1b,NoAI:1b,Health:1024f,Team:"NoCollision",Tags:["LeftClick"],Attributes:[{Name:"generic.max_health",Base:1024}]}
execute as @a at @s run summon vex ~ ~0.815 ~ {Silent:1b,NoAI:1b,Health:1024f,Team:"NoCollision",Tags:["LeftClick"],Attributes:[{Name:"generic.max_health",Base:1024}]}
execute as @a at @s run summon vex ~0.205 ~1 ~ {Silent:1b,NoAI:1b,Health:1024f,Team:"NoCollision",Tags:["LeftClick"],Attributes:[{Name:"generic.max_health",Base:1024}]}
execute as @a at @s run summon vex ~-0.205 ~1 ~ {Silent:1b,NoAI:1b,Health:1024f,Team:"NoCollision",Tags:["LeftClick"],Attributes:[{Name:"generic.max_health",Base:1024}]}
execute as @a at @s run summon vex ~ ~1 ~0.205 {Silent:1b,NoAI:1b,Health:1024f,Team:"NoCollision",Tags:["LeftClick"],Attributes:[{Name:"generic.max_health",Base:1024}]}
execute as @a at @s run summon vex ~ ~1 ~-0.205 {Silent:1b,NoAI:1b,Health:1024f,Team:"NoCollision",Tags:["LeftClick"],Attributes:[{Name:"generic.max_health",Base:1024}]}
```
参考：ちゅずめさんの検証Tweet

https://twitter.com/Maybe_Chuzume/status/1636332689354792960/

なぜ対策できるかまでは検証できていませんが、プレイヤーの攻撃判定が出る場所 (目前) に検知用のモブが存在しなければ、どうやらチャンク跨ぎの問題を高確率で回避できるみたいです。<font color="#D01919">ただ位置関係によっては、チャンクの境目辺りで判定のすり抜けが起こる場合も確認しています。</font>

また、この実装においてはプレイヤー目前にモブがいない状態となるので、[次の問題が生じます](#攻撃判定がブロックに吸われてしまう)。

## <font color="#D01919">攻撃判定がブロックに吸われてしまう</font>

プレイヤー目前に検知用のモブがいない場合、攻撃判定が「背の高い草」などの通り抜け可能なブロックに吸われることにより、モブにダメージを与えられなります。

![leftclick8.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3589374/e0f6a495-a163-3511-aa1b-56e25c30e51f.gif)

<font color="#888888">攻撃判定が背の高い草に吸われ、左クリックの検知ができない</font>

### 対策①: ブロック内／外にいる場合で処理を分ける

プレイヤーの目前にヴェックスを1体召喚する手法では攻撃判定はブロックに吸われることなく、左クリックの検知が可能です。**ブロック内**にいる場合はヴェックスを1体、**ブロック外**にいる場合はヴェックスを6体囲うように召喚するといった場合分けをしていきます。

```json:<font color="#C3CAD5">advent:air (空気ブロックをまとめたブロックタグ)</font>
{
    "values": [
        "air",
        "cave_air",
        "void_air",
        "light"
    ]
}
```
``` mcfunction:<font color="#C3CAD5">advent:leftclick/summon </font>
tp @e[type=vex,tag=LeftClick] ~ -1000 ~
execute as @a at @s anchored eyes positioned ^ ^ ^ unless block ~ ~ ~ #advent:air run summon vex ~ ~-0.6 ~ {Silent:1b,NoAI:1b,Health:1024f,Team:"NoCollision",Tags:["LeftClick"],Attributes:[{Name:"generic.max_health",Base:1024}]}
execute as @a at @s anchored eyes positioned ^ ^ ^ if block ~ ~ ~ #advent:air run summon vex ~ ~-0.005 ~ {Silent:1b,NoAI:1b,Health:1024f,Team:"NoCollision",Tags:["LeftClick"],Attributes:[{Name:"generic.max_health",Base:1024}]}
execute as @a at @s anchored eyes positioned ^ ^ ^ if block ~ ~ ~ #advent:air run summon vex ~ ~-0.805 ~ {Silent:1b,NoAI:1b,Health:1024f,Team:"NoCollision",Tags:["LeftClick"],Attributes:[{Name:"generic.max_health",Base:1024}]}
execute as @a at @s anchored eyes positioned ^ ^ ^ if block ~ ~ ~ #advent:air run summon vex ~0.205 ~-0.62 ~ {Silent:1b,NoAI:1b,Health:1024f,Team:"NoCollision",Tags:["LeftClick"],Attributes:[{Name:"generic.max_health",Base:1024}]}
execute as @a at @s anchored eyes positioned ^ ^ ^ if block ~ ~ ~ #advent:air run summon vex ~-0.205 ~-0.62 ~ {Silent:1b,NoAI:1b,Health:1024f,Team:"NoCollision",Tags:["LeftClick"],Attributes:[{Name:"generic.max_health",Base:1024}]}
execute as @a at @s anchored eyes positioned ^ ^ ^ if block ~ ~ ~ #advent:air run summon vex ~ ~-0.62 ~0.205 {Silent:1b,NoAI:1b,Health:1024f,Team:"NoCollision",Tags:["LeftClick"],Attributes:[{Name:"generic.max_health",Base:1024}]}
execute as @a at @s anchored eyes positioned ^ ^ ^ if block ~ ~ ~ #advent:air run summon vex ~ ~-0.62 ~-0.205 {Silent:1b,NoAI:1b,Health:1024f,Team:"NoCollision",Tags:["LeftClick"],Attributes:[{Name:"generic.max_health",Base:1024}]}
```

ただ、現状は<font color="#D01919">スニークを連打した際や高速で落下した際、一瞬ブロックに攻撃判定が吸われる／左クリック検知がすり抜ける場合も確認できています。</font>1.20.5で追加される予定のgeneric.scaleでヴェックスのサイズを変更することで更なる対策ができる可能性もありますが、未だ検証はできていません。正式リリース後、検証しようと考えています。

<font color="#D01919">また、**ブロック内にいる場合**での攻撃において、[チャンクを跨いだ条件を満たしてしまう場合](#特定の条件下で検知用モブへの攻撃が外れる)、検知用モブへの攻撃が外れてしまいます。</font>とかなんとか... 問題が多いです。

### 対策②: ワールド内にプレイヤーに被るようなブロックを置かない

こういう対策をするか... って思わされるほど悩んでます。つまるところ、この対策ができない、バニラのワールドに導入させるようなデータパックにおいては、今のところ[攻撃判定がブロックに吸われてしまう](#攻撃判定がブロックに吸われてしまう)問題の完全解決ができていない訳です。ヴェックスのテクスチャも消してるしな...。

## <font color="#D01919">稀に検知用のモブの位置がずれ、攻撃が外れる</font>

この問題が一番の問題児で、3年経っても完璧な左クリック検知ができていない主な原因でもあります。<font color="#D01919">稀に検知用モブへの攻撃が外れる場合がある</font>んです。

正確な原因は未だ解明できていませんが、サーバーとクライアントの同期ズレによって検知用モブを召喚位置がずれることによって (？)、クリックが検知できなくなってしまう場合があります。**同期ズレが起きていない場合は完璧にクリック検知ができている**ので、これさえ直れば... とずっと思っています。

![leftclick8.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3589374/3a59ff78-c292-7935-2a03-7590d491250a.gif)
<font color="#888888">稀に数tick前の時点 (右) にグッとずれるぞ！</font>

### 対策: 検知用モブを数tick後にkillする／検知用モブを増やす

**対策なんてないよ！！！** って投げ出したいところですが、現状私は1tick→3tick程後に検知用モブをkillすることや二重に召喚することでお茶を濁せているので、粗くはありますが対策と言えるかもしれません。

# おまけ: Interactionが惜しい

**「マップ製作に使える！左クリックと右クリックが検知できる！！」** と大々的に1.19.4に登場したInteractionエンティティですが、仕様が惜しく、個人的にはヴェックスから乗り換えることはありませんでした。クリック検知の無効化などのカスタマイズ性が充実していたらどれだけよかったか。

* <font color="#D01919">問答無用で左クリックと右クリックを判定／吸収してしまう</font>
* <font color="#D01919">player_hurt_entityにおいて与えたダメージが1として判定される</font>


**無条件でマウスの左クリック／右クリック／キーの押下を検知させてくれもやん...！**

# さいごに

本当であれば完璧な左クリック検知のやり方として投稿をしたかったのですが、無念...。
今はアップデートで何か代替案が出てくるのを待っている状態ではありますが、もしこうすれば左クリック検知ができそうじゃないか？といった考えがあれば共有していただけると大変ありがたいです。~~助けてくれ～～～～！！！~~

ちなみに左クリック検知を含んだ開発中のマイクラRPG配布マップのデータパックはCC0 **(著作権フリー！)** で公開しているので、同じ道を歩もうとしている方がいればぜひ覗いてみて下さい。ｿﾚﾃﾞﾊﾏﾀ~

https://github.com/HamaSSH/RPG-Datapack