(Java Minecraft 1.14.2) How to detect a player changing the direction they are looking in?

Solution 1:

You can use this in a datapack, it uses two functions

The setup function should be executed once:

#tracks horizontal rotation
scoreboard objectives add DM_rotOne dummy

#tracks vertical rotation
scoreboard objectives add DM_rotTwo dummy

#tracks the distance rotated within one tick
scoreboard objectives add DM_distance dummy

#used for maths
scoreboard objectives add DM_placeholder dummy

#hold the values from the previous tick, also used for maths
scoreboard objectives add DM_lastRotOne dummy
scoreboard objectives add DM_lastRotTwo dummy
scoreboard objectives add DM_lastInput dummy

#flags when an input should be registered
scoreboard objectives add DM_registerInput dummy

#tracks the input in what direction the player moved
#is 0 if the player didn't move fast enough
scoreboard objectives add DM_input dummy

#used as timer to know if a new DM_input value should be registered
#and to detect if a player doesn't move for a while
scoreboard objectives add DM_timer dummy

#use these to remember previous inputs, add as many as you need
scoreboard objectives add DM_stepOne dummy
scoreboard objectives add DM_stepTwo dummy
scoreboard objectives add DM_stepThree dummy
scoreboard objectives add DM_stepFour dummy

#add constant values, these should not be changed!
scoreboard objectives add DM_constant dummy
scoreboard players set minusOne DM_constant -1
scoreboard players set noMovement DM_constant 0
scoreboard players set up DM_constant 1
scoreboard players set down DM_constant 2
scoreboard players set right DM_constant 3
scoreboard players set upRight DM_constant 4
scoreboard players set downRight DM_constant 5
scoreboard players set left DM_constant 6
scoreboard players set upLeft DM_constant 7
scoreboard players set downLeft DM_constant 8

#forces 0 as input, can be used together with timerEnabled=0 to break a chain of inputs
scoreboard objectives add DM_forceZero dummy

#A trigger scoreboard objective that can be set to 1, or 0 by each individual player, 
#defaults to 0, confirms inputs in chat for players where it is set to 1
scoreboard objectives add DM_confDirection trigger

#add parameters, these change how the main function operates
scoreboard objectives add DM_parameter dummy

#Defines how fast you have to rotate for it to count as an input
scoreboard players set speedRequired DM_parameter 2500000

#Defines how fast you can maximaly move before it counts as not moving
scoreboard players set maximalRotationWhenNotMoving DM_parameter 1500000

#Defines how long you have to not move before it counts as an "input"
scoreboard players set timerTime DM_parameter 50

#Defines how long you have to not move before you can make a new input
#this is used to prevent multiple inputs when the player only attempts to make one input
scoreboard players set timeBeforeRegisteringNewInput DM_parameter 2

#enables the input for "not moving", set this to 0 to only allow directional inputs
scoreboard players set timerEnabled DM_parameter 1

#disables the detection of diagonal movements when set to 0
scoreboard players set detectDiagonals DM_parameter 1

Note: the DM_ is for your namespace abbreviation - it's good practice to put one before all scoreboards and tags and such so they don't conflict with someone else's datapack if a player has more than one installed. Just use the replace function in notepad to change them all to the appropriate letters for your namespace.

The main function should be executed every tick:

#get player rotation
execute as @a store result score @s DM_rotOne run data get entity @s Rotation[0] 100
execute as @a store result score @s DM_rotTwo run data get entity @s Rotation[1] 100

#error correction right after logging in to the server, may result in one false input per sitting
execute as @a[scores={DM_rotOne=..-1}] run scoreboard players add @s DM_rotOne 36000

#prevent overflow, or underflow, where the rotation jumps from 35999 to 0, or 0 to 35999, because that's how circles work
execute as @a[scores={DM_rotOne=..9000,DM_lastRotOne=27000..}] run scoreboard players remove @s DM_lastRotOne 36000
execute as @a[scores={DM_rotOne=27000..,DM_lastRotOne=..9000}] run scoreboard players add @s DM_lastRotOne 36000

#calculate relative rotation compared to previous tick
execute as @a run scoreboard players operation @s DM_lastRotOne -= @s DM_rotOne
execute as @a run scoreboard players operation @s DM_lastRotTwo -= @s DM_rotTwo

#calculate the relative distance rotated (The square of the distance is used)
execute as @a run scoreboard players operation @s DM_distance = @s DM_lastRotOne
execute as @a run scoreboard players operation @s DM_distance *= @s DM_lastRotOne
execute as @a run scoreboard players operation @s DM_placeholder = @s DM_lastRotTwo
execute as @a run scoreboard players operation @s DM_placeholder *= @s DM_lastRotTwo
execute as @a run scoreboard players operation @s DM_distance += @s DM_placeholder

#determine the input
execute as @a run scoreboard players set @s DM_input 0

#determine diagonals, gets the correct result for horizontal and vertical movement if you rotate very precisely
execute as @a[scores={DM_lastRotTwo=1..}] if score @s DM_distance >= speedRequired DM_parameter run scoreboard players operation @s DM_input += up DM_constant
execute as @a[scores={DM_lastRotOne=..-1}] if score @s DM_distance >= speedRequired DM_parameter run scoreboard players operation @s DM_input += right DM_constant
execute as @a[scores={DM_lastRotTwo=..-1}] if score @s DM_distance >= speedRequired DM_parameter run scoreboard players operation @s DM_input += down DM_constant
execute as @a[scores={DM_lastRotOne=1..}] if score @s DM_distance >= speedRequired DM_parameter run scoreboard players operation @s DM_input += left DM_constant

#error correction for input, less precision required for up/down
execute as @a run scoreboard players operation @s DM_placeholder = @s DM_lastRotOne
execute as @a if score detectDiagonals DM_parameter matches 1 run scoreboard players operation @s DM_placeholder += @s DM_lastRotOne
execute as @a[scores={DM_lastRotTwo=1..,DM_lastRotOne=1..}] if score @s DM_distance >= speedRequired DM_parameter if score @s DM_lastRotTwo >= @s DM_placeholder run scoreboard players operation @s DM_input = up DM_constant
execute as @a[scores={DM_lastRotTwo=..-1,DM_lastRotOne=..-1}] if score @s DM_distance >= speedRequired DM_parameter if score @s DM_lastRotTwo <= @s DM_placeholder run scoreboard players operation @s DM_input = down DM_constant
execute as @a run scoreboard players operation @s DM_placeholder *= minusOne DM_constant
execute as @a[scores={DM_lastRotTwo=1..,DM_lastRotOne=..-1}] if score @s DM_distance >= speedRequired DM_parameter if score @s DM_lastRotTwo >= @s DM_placeholder run scoreboard players operation @s DM_input = up DM_constant
execute as @a[scores={DM_lastRotTwo=..-1,DM_lastRotOne=1..}] if score @s DM_distance >= speedRequired DM_parameter if score @s DM_lastRotTwo <= @s DM_placeholder run scoreboard players operation @s DM_input = down DM_constant

#error correction for input, less precision required for left/right
execute as @a run scoreboard players operation @s DM_placeholder = @s DM_lastRotTwo
execute as @a if score detectDiagonals DM_parameter matches 1 run scoreboard players operation @s DM_placeholder += @s DM_lastRotTwo
execute as @a[scores={DM_lastRotTwo=1..,DM_lastRotOne=1..}] if score @s DM_distance >= speedRequired DM_parameter if score @s DM_lastRotOne >= @s DM_placeholder run scoreboard players operation @s DM_input = left DM_constant
execute as @a[scores={DM_lastRotTwo=..-1,DM_lastRotOne=..-1}] if score @s DM_distance >= speedRequired DM_parameter if score @s DM_lastRotOne <= @s DM_placeholder run scoreboard players operation @s DM_input = right DM_constant
execute as @a run scoreboard players operation @s DM_placeholder *= minusOne DM_constant
execute as @a[scores={DM_lastRotTwo=1..,DM_lastRotOne=..-1}] if score @s DM_distance >= speedRequired DM_parameter if score @s DM_lastRotOne <= @s DM_placeholder run scoreboard players operation @s DM_input = right DM_constant
execute as @a[scores={DM_lastRotTwo=..-1,DM_lastRotOne=1..}] if score @s DM_distance >= speedRequired DM_parameter if score @s DM_lastRotOne >= @s DM_placeholder run scoreboard players operation @s DM_input = left DM_constant

#set DM_registerInput flag if valid input is detected
scoreboard players set @a DM_registerInput 0
execute as @a[scores={DM_input=1..}] if score @s DM_timer > timeBeforeRegisteringNewInput DM_parameter run scoreboard players set @s DM_registerInput 1
execute as @a if score @s DM_input = @s DM_lastInput if score @s DM_timer = repeatedInputTime DM_parameter run scoreboard players set @s DM_registerInput 1

#increase timer
execute as @a if score @s DM_distance <= maximalRotationWhenNotMoving DM_parameter run scoreboard players add @s DM_timer 1
#set timer to 0 if there was an input
execute as @a[scores={DM_input=1..}] run scoreboard players set @s DM_timer 0
#sets the DM_registerInput flag if the timer has reached the value for when "no movement" should be detected
execute as @a if score @s DM_timer = DM_timerTime DM_parameter if score DM_timerEnabled DM_parameter matches 1 run scoreboard players set @s DM_registerInput 1

#remember values for next tick
execute as @a run scoreboard players operation @s DM_lastRotOne = @s DM_rotOne
execute as @a run scoreboard players operation @s DM_lastRotTwo = @s DM_rotTwo
execute as @a[scores={DM_registerInput=1}] run scoreboard players operation @s DM_lastInput = @s DM_input

#forceZero if DM_forceZero has been set (use '/scoreboard players set @a DM_forceZero 1' to set this parameter; it is automatically reset)
execute as @a[scores={DM_forceZero=1}] run scoreboard players set @s DM_input 0
execute as @a[scores={DM_forceZero=1}] run scoreboard players set @s DM_registerInput 1
scoreboard players set @a DM_forceZero 0

#confirms input to players who have the scoreboard 'DM_confDirection' set to 1, can be changed with a trigger command
scoreboard players enable @a DM_confDirection
execute as @a[scores={DM_registerInput=1,DM_confDirection=1}] if score @s DM_input = up DM_constant run msg @s I look up
execute as @a[scores={DM_registerInput=1,DM_confDirection=1}] if score @s DM_input = left DM_constant run msg @s I look left
execute as @a[scores={DM_registerInput=1,DM_confDirection=1}] if score @s DM_input = right DM_constant run msg @s I look right
execute as @a[scores={DM_registerInput=1,DM_confDirection=1}] if score @s DM_input = down DM_constant run msg @s I look down
execute as @a[scores={DM_registerInput=1,DM_confDirection=1}] if score @s DM_input = upRight DM_constant run msg @s I look upRight
execute as @a[scores={DM_registerInput=1,DM_confDirection=1}] if score @s DM_input = upLeft DM_constant run msg @s I look upLeft
execute as @a[scores={DM_registerInput=1,DM_confDirection=1}] if score @s DM_input = downRight DM_constant run msg @s I look downRight
execute as @a[scores={DM_registerInput=1,DM_confDirection=1}] if score @s DM_input = downLeft DM_constant run msg @s I look downLeft
execute as @a[scores={DM_registerInput=1,DM_confDirection=1}] if score @s DM_input = noMovement DM_constant run msg @s I stopped

#Record valid inputs, you can add more steps according to this pattern if you have enough scoreboard objetives
execute as @a[scores={DM_registerInput=1}] run scoreboard players operation @s DM_stepOne = @s DM_stepTwo
execute as @a[scores={DM_registerInput=1}] run scoreboard players operation @s DM_stepTwo = @s DM_stepThree
execute as @a[scores={DM_registerInput=1}] run scoreboard players operation @s DM_stepThree = @s DM_stepFour
execute as @a[scores={DM_registerInput=1}] run scoreboard players operation @s DM_stepFour = @s DM_input

#Do something if a chain of correct inputs has been made
#for the input chain 'right, left, up, down' you can use this command
execute as @a[scores={DM_registerInput=1}] if score @s DM_stepOne = right DM_constant if score @s DM_stepTwo = left DM_constant if score @s DM_stepThree = up DM_constant if score @s DM_stepFour = down DM_constant run say I activated skill one
#for the input chain 'upLeft, upRight, downLeft, downRight' you can use this command (the DM_parameter 'detectDiagonals' has to be 1 for this to be possible 
execute as @a[scores={DM_registerInput=1}] if score @s DM_stepOne = upLeft DM_constant if score @s DM_stepTwo = upRight DM_constant if score @s DM_stepThree = downLeft DM_constant if score @s DM_stepFour = downRight DM_constant run say I activated skill two
#for the input chain 'left, no movement, left, left' you can use this command (the DM_parameter 'detectDiagonals' has to be 1 for this to be possible 
execute as @a[scores={DM_registerInput=1}] if score @s DM_stepOne = left DM_constant if score @s DM_stepTwo = left DM_constant if score @s DM_stepThree = left DM_constant if score @s DM_stepFour = left DM_constant run say I activated skill three
#To make shorter input chains you would only use the last few steps, ignoring step one.
#This command would be for 'right, right'
execute as @a[scores={DM_registerInput=1}] if score @s DM_stepThree = right DM_constant if score @s DM_stepFour = right DM_constant run say I activated skill four

Solution 2:

I do not know much about datapacks, but a function like this should work, even in multiplayer. All the commands can go into the same function and the function should be executed every tick.

This setup uses these dummy objectives:

  • rotationOne (for y rotation)
  • rotationTwo (for x rotation)
  • lastRotationOne
  • lastRotationTwo
  • timer
  • looksLeft
  • looksRight
  • looksUp
  • looksDown
  • directionChanged
  • lastDirection (holds a value based on what kind of rotation the player did last)
  • stepOne, stepTwo,... (for "remembering" the last few actions, you can add as many as you want)
  • global (for holding parameters)

rotationOne bascially devides a ring around the player into sectors (numbered 0-12 if the number in the first command is 0.036), if a player looked into one sector in one tick, and a different one in the next, then that gets registered as looking right, or left. Imidiately after loading the map these values seem to be negative for a while, this problem seems to resolve itself after a few seconds.

timer is a timer that is used to detect if the player did NOT rotate for a while

global holds the parameters min, max, left, right, up, down, and timerTime.
min and max are used for the sectors, min should always be 0, max is the highest value that rotationOne can have before rolling over to 0. This changes if you change the number in the first command. Make sure to keep max updated if you decide to change the number in the first command.
timerTime is used by the timer and defines how long a player has to stand still until it counts as "not moving". A value of 20 sets the timer to 1 second.
left, right, up, and down are values assigned to each direction, I recommend using 1, 2, 4, and 8. The value for "no movement" will always be 0

Using these values you would initialize your scoreboards like so:

scoreboard objectives add rotationOne dummy
scoreboard objectives add rotationTwo dummy
scoreboard objectives add lastRotationOne dummy
scoreboard objectives add lastRotationTwo dummy
scoreboard objectives add timer dummy
scoreboard objectives add looksLeft dummy
scoreboard objectives add looksRight dummy
scoreboard objectives add looksUp dummy
scoreboard objectives add looksDown dummy
scoreboard objectives add directionChanged dummy
scoreboard objectives add lastDirection dummy
scoreboard objectives add stepOne dummy
scoreboard objectives add stepTwo dummy
scoreboard objectives add stepThree dummy
scoreboard objectives add stepFour dummy
scoreboard objectives add stepFive dummy
scoreboard objectives add stepSix dummy
scoreboard objectives add global dummy

scoreboard players set min global 0
scoreboard players set max global 12
scoreboard players set left global 1
scoreboard players set right global 2
scoreboard players set up global 4
scoreboard players set down global 8
scoreboard players set timerTime global 50 (for 2.5 seconds)

In the first two commands you decide the precision, a higher number at the end will require the movements to be more precise, lower numbers will require bigger movements. In the first command that means that there will be more, or less sectors in the ring. Make sure to set max to the correct number if you change the first command. The first two commands also store the current rotation of each player, which will be used later.

execute as @a store result score @s rotationOne run data get entity @s Rotation[0] 0.036
execute as @a store result score @s rotationTwo run data get entity @s Rotation[1] 0.036

After that you can run the following commands. You should not have to change anything about them, though they could probably be more efficient.

execute as @a unless score @s rotationOne = min global store success score @s looksLeft if score @s lastRotationOne > @s rotationOne
execute as @a if score @s rotationOne = min global store success score @s looksLeft unless score max global = @s lastRotationOne unless score min global = @s lastRotationOne
execute as @a if score @s rotationOne = max global store success score @s looksLeft if score min global = @s lastRotationOne
execute as @a unless score @s rotationOne = max global store success score @s looksRight if score @s lastRotationOne < @s rotationOne
execute as @a if score @s rotationOne = max global store success score @s looksRight unless score min global = @s lastRotationOne unless score max global = @s lastRotationOne
execute as @a if score @s rotationOne = min global store success score @s looksRight if score max global = @s lastRotationOne
execute as @a store success score @s looksUp if score @s lastRotationTwo > @s rotationTwo
execute as @a store success score @s looksDown if score @s lastRotationTwo < @s rotationTwo
execute as @a store success score @s directionChanged unless score @s rotationOne = @s lastRotationOne
execute as @a[scores={directionChanged=0}] store success score @s directionChanged unless score @s rotationTwo = @s lastRotationTwo
execute as @a run scoreboard players operation @s lastRotationOne = @s rotationOne
execute as @a run scoreboard players operation @s lastRotationTwo = @s rotationTwo
scoreboard players remove @a timer 1
execute as @a[scores={directionChanged=1}] run scoreboard players operation @s timer = timerTime global
execute as @a[scores={timer=..0}] run scoreboard players set @s directionChanged 1
execute as @a[scores={timer=..0}] run scoreboard players operation @s timer = timerTime global
execute as @a[scores={directionChanged=1}] run scoreboard players set @s lastDirection 0
execute as @a[scores={looksLeft=1}] run scoreboard players operation @s lastDirection = left global
execute as @a[scores={looksRight=1}] run scoreboard players operation @s lastDirection = right global
execute as @a[scores={looksUp=1}] run scoreboard players operation @s lastDirection = up global
execute as @a[scores={looksDown=1}] run scoreboard players operation @s lastDirection = down global
execute as @a if score @s lastDirection = @s stepFive run scoreboard players set @s directionChanged 0

To confirm to a player, when a direction has been registered, you would use these commands:

execute as @a[scores={directionChanged=1,looksLeft=1}] run msg @s left
execute as @a[scores={directionChanged=1,looksRight=1}] run msg @s right
execute as @a[scores={directionChanged=1,looksUp=1}] run msg @s up
execute as @a[scores={directionChanged=1,looksDown=1}] run msg @s down    
execute as @a[scores={directionChanged=1,lastDirection=0}] run msg @s stopped 

At this point you should decide how many inputs you want to use for your most complicated ability and add as many as you need according to this pattern:

execute as @a[scores={directionChanged=1}] run scoreboard players operation @s stepOne = @s stepTwo
execute as @a[scores={directionChanged=1}] run scoreboard players operation @s stepTwo = @s stepThree
execute as @a[scores={directionChanged=1}] run scoreboard players operation @s stepThree = @s stepFour
execute as @a[scores={directionChanged=1}] run scoreboard players operation @s stepFour = @s stepFive
execute as @a[scores={directionChanged=1}] run scoreboard players operation @s stepFive = @s lastDirection

And finally you get to make the abilities, you basically just make a long command with a lot of ifs, one for every step that is important for activating that ability. For an ability that should activate after "left, right, not moving, up, down" you would use a command like this:

execute as @a[scores={directionChanged=1}] if score @s stepOne = left global if score @s stepTwo = right global if score @s stepThree matches 0 if score @s stepFour = up global if score @s stepFive = down global run <abilityOne>

If you want to add more abilities that are shorter, then you start at a higher step and ignore the first few steps, to make an ability for "up, down, up" you can use this command:

execute as @a[scores={directionChanged=1}] if score @s stepThree = up global if score @s stepFour = down global if score @s stepFive = up global run <abilityTwo>

I used all of these commands in that order in a long chain of command blocks. I hope you can just copy and paste them into a function file.