How can I trigger a command when a player is hit by a Snowball?

I am trying to make a snowball that triggers a command when it hits a player in 1.15, but I don't know how to do that. When I do execute at @e[type=snowball] run gamemode spectator @a[distance=..2] it only runs the command if it goes right next to them.


Solution 1:

Detecting where a snowball hit a player or block is tricky. You can't do something at the position of something that doesn't exist anymore, a snowball never gets the OnGround tag, the view direction of a snowball is completely unrelated to its flight direction and not even the advancement trigger entity_hurt_player works for it.

So what you need to do instead is moving a dummy entity with it, keep checking if there is still a snowball nearby and if not, assume that it has hit something or someone. You can for example do that like this (all commands executed every tick):

/execute as @e[type=armor_stand,tag=tracker] at @s run tp @s @e[type=snowball,tag=tracked,distance=..2,sort=nearest,limit=1]
/execute at @e[type=snowball,tag=!tracked] run summon armor_stand ~ ~ ~ {Tags:["tracker"],Marker:1,NoAI:1,NoGravity:1,Invisible:1}
/tag @e[type=snowball,tag=!tracked] add tracked
/execute at @e[type=armor_stand,tag=tracker] unless entity @e[type=snowball,distance=..2] run …
/execute as @e[type=armor_stand,tag=tracker] unless entity @e[type=snowball,distance=..2] run kill @s

Explanation:
It's easiest to explain this system in a different order than it is there, because that order is made for performance and responsiveness.
The second command creates a dummy armour stand at every snowball that doesn't have one yet.
The third command then tags that snowball as one that already got its dummy armour stand.
The first command keeps teleporting the armour stand to the snowball, as long as the snowball still exists.
The fourth command is the one you actually care about: When the armour stand can't find a snowball nearby anymore, we assume that that's because it was broken by the ground, a player, etc. You can execute whatever command here and it will happen at the last place that the snowball existed at (so still in air, not exactly at the block).
The fifth command then removes the armour stand so that you don't have lots of useless armour stands lingering around.


Issues and improvements

  • In my tests the radius of 2m always included the snowball, even if it was falling straight down. But maybe there can be cases that cause it to fly faster (for example with the help of explosions) or you also want to be able to teleport the snowball around. You could link the snowball and armour stand together more thoroughly using IDs. That can either be a custom ID system using scoreboards (tutorials for that can be found online) or using the UUID of the snowball. In 1.16+ you can compare the entire UUID relatively easily, in 1.15- you need to scale it down to get parts of it or you can just hope that you don't get any UUID collisions if you only compare the lower half of the number (because the UUID is too long to be stored in a scoreboard, so you get an overflow, which should still usually give you the same result only if the original number was the same, but it's not guaranteed).
  • The snowball could also disappear because it fell into the void, got removed with /kill or because it flew into unloaded chunks. In those cases you will probably not want to do something at that position. There is no good way to catch all of these cases, so you'll have to handle them all individually, if you want to be that precise.
  • If you want to be more precise with the location of where the snowball hit (instead of being one tick behind), you can try removing Marker, NoAI and NoGravity from the armour stand and also copying the Motion tag from the snowball to the armour stand. In my tests that had promising results, but I am not sure if it will always work well. And of course you need to delay executing your command and killing the armour stand by one tick then, but that can easily be done by moving the fourth and fifth command to the start of the chain.
    If this doesn't work, you can try moving the armour stand "manually" (with teleportation) according to the snowball's Motion tag. You could also try half of that, to get the average location of the range where it could have hit something. Or you could even do a fancy block detection with raycasting. /tp with a facing argument is probably helpful for rotating something from one position to the next in that case.
  • You could also consider alternatives to snowballs. For example arrows have a damage tag that you could set to 0, they stick around after they have landed, they trigger entity_hurt_player and they have a boolean inGround tag and a complex inBlockState tag that can be used for many things.