Minecraft – Function bullets/guns hitting blocks lag

minecraft-commandsminecraft-java-edition

Simply put, I am making guns and have run into a serious issue.
To start off I will have to explain how I made them.

So I made the guns by using a function. When you right click with a carrot on a stick with a specific model data it shoots, depending on the model data. Currently I have 2 working guns, a sniper and a shotgun. It is the latter with which the issue really started.
This is because of how I made it so that bullets don't go through blocks. I made a seperate function which gives the "bullet" a score when it is in a block it is allowed to pass through. When it comes in a block it isn't allowed to pass through it is killed.

The issue comes when calling this function in the gun's shoot function.
When I fire, the following happens:

  1. The bullet(s) is(are) spawned
  2. The bullet(s) teleport based on the nearest player's direction (the shooter)
  3. The following repeats a lot of times (depending on the gun's range):
Particle
Give nearby entities "hit" score 
Call blockhitcheck function 
Teleport forward 0.25 blocks

For the sniper for example, this repeats 400 times (100 range) as more range started to create the issue.
For the shotgun each bullet has seperate "hit" score so the damage could easily be counted together.So the exact look of the command differs quite a bit per gun, but the order is the same.

So, what issue does it actually create?
A real weird kind of lag. When I give the sniper a lot of range it seems to create constant lag for everything in the world, yet the player seems unaffected. (so much lag I could see the bullet travel, which is supposed to happen all within a single game tick)
With the shotgun 5 blocks range was enough to create this lag (only when fired) and upon removing the block checking it went back to normal. So I'm looking for a better, less laggy way to have bullets that don't just pass through every block and don't lag quickly.

I may have explained some things in a weird way, so if you don't understand what I mean feel free to ask.

Edit: I should mention that I have only basic function knowledge, I can only do things I could do with command blocks, just in larger quantities.

Edit: Here's some of the commands.

This is one example of the 222 "whitelisted" blocks, each is the same command though.

execute as @e[type=armor_stand,tag=bullet] at @s if block ~ ~ ~ air run scoreboard players set @s notinblock 5

This is then done after all the block checks to kill if it isn't in one of the "passable" blocks and then reset the score to check again.

kill @e[type=armor_stand,tag=bullet,scores={notinblock=0..2}]
scoreboard players set @s notinblock 0

And then for the actual shot.

This summons the bullet.

execute as @a[scores={shoot=1..,shootCD=..1},nbt={SelectedItem:{id:"minecraft:carrot_on_a_stick",tag:{CustomModelData:1}}}] at @s anchored eyes run summon armor_stand ^-0.25 ^-0.15 ^0.65 {Invisible:1b,Invulnerable:1b,PersistenceRequired:1b,NoGravity:1b,Small:1b,Tags:["bullet","sniperbullet"]}

This rotates the bullet in the shooter's direction.

execute as @e[type=armor_stand,tag=sniperbullet] at @s rotated as @a[limit=1,sort=nearest,nbt={SelectedItem:{id:"minecraft:carrot_on_a_stick",tag:{CustomModelData:1}}}] run tp ^ ^ ^

This is for hitting targets (slightly different from actual command to shorten it, works the same)

execute as @e[type=armor_stand,tag=sniperbullet] at @s run scoreboard players set @e[distance=..0.5] sniperhit 1

Then it runs the blockhitcheck function to see if it is in a block.

execute as @e[type=armor_stand,tag=sniperbullet] at @s run function grinn:blockhitcheck

Afterwards it teleports if not killed by blockhitcheck.

execute as @e[type=armor_stand,tag=sniperbullet] at @s run tp @s ^ ^ ^0.25

These last three repeat a number of times depending on how much range I wish the gun to have (ex. 400x for 100 blocks range)

I left out a lot of things like particles and sound effects, also the parts that kill/damage the targets that have been hit.

Clarity Edit: In case it wasn't obvious what caused the lag, it's the blockhitcheck function, removing it from my shoot function allows it to be used pretty much lagless.

Best Answer

I haven't really payed much attention to anything except your block check, because you said that you already knew that the problem was in there. So here is my feedback on only that part:

Firstly, you check every block in a separate command. That means that even if one matching type of block is found, it still needlessly checks every other block as well, even though it can't be that. Basically something like this:

tag @s remove match
execute if block ~ ~-1 ~ stone run tag @s add match
execute if block ~ ~-1 ~ dirt run tag @s add match
execute if block ~ ~-1 ~ gravel run tag @s add match
execute if entity @s[tag=match] run say matched

If you're standing on stone, the second and third block check are needlessly executed. Minecraft's function optimisation might see that you would get a tag that you already have and skip the check, but I think they are currently only optimised once when loaded and not during execution.


Idea #1: Invert both the tagging and the condition so that you can put it all into one command:

tag @s add match
execute unless block ~ ~-1 ~ stone unless block ~ ~-1 ~ dirt unless block ~ ~-1 ~ gravel run tag @s remove match
execute if entity @s[tag=match] run say matched

The /execute unless command fails as soon as a matching block is found, so it doesn't need to do all the other checks anymore.

Here is a screenshot of the performance difference this has.
For this test I let a repeating command block execute the function 216 times per tick and the function checked for every single block that existed in 1.14.4, except air, in alphabetical order (because I happened to have something similar lying around already).
The red section at the beginning is the old function, with one command per block, while standing on the crafting table (checked early in the function). The yellow-lime area is when I stood on the crafting table with the new function. The orange area is when I stood on the sandstone. The red area at the end is when I stood on the crying obsidian, which is not in the list. But even there the performance is better than with the old function.

performance graph idea 1


Idea #2: A block tag. In the location <datapack>/data/<namespace>/tags/blocks create a .json file, for example I name mine "matching.json" and used the namespace "test". It should look similar to this:

{
 "values":[
  "stone",
  "dirt",
  "gravel"
 ]
}

Then you can reduce the huge command in the function to a really short one:

tag @s remove match
execute if block ~ ~-1 ~ #test:matching run tag @s add match
execute if entity @s[tag=match] run say matched

Or just this:

execute if block ~ ~-1 ~ #test:matching run say matched

This last variant works if you don't want to do multiple things with that check. But even if you do, you can also just call another function, that way you don't have to do the check multiple times either. Which one is better in terms of code quality is a different discussion, they don't really differ in performance.

Here is a screenshot of the performance difference this has. The yellow-lime area at the beginning is with the implementation of idea #1, the second one with idea #2, first standing on the crafting table, then on sandstone, then on the crying obsidian (I can see no difference in performance between those three, the differences that are visible are probably just random fluctuations).

performance graph idea 2

You can find a list of all blocks in 1.14.4 in my answer here, already in the format with quotation marks and commas that you need. I'll update that command to 1.15 soon™ and to 1.16 once it is fully released.


Another remark: A tick time of less than 100ms at all times, even without the optimisations in this post, is not bad for 216 checks for 672 types of blocks (~0.0007 ms per check, so with your datapack on my computer, I would start getting lag with 327 bullets). I had to explicitly bloat the number of checks by using as @a as @a as @a (with 5 paintings nearby) to even get to a point where I saw performance differences. My computer is pretty fast, but maybe your real issue is that you have too many bullets around. Are you sure that you're successfully killing them when they have arrived at their destination? Also keep in mind that in some situations even dying mobs can still be detected for 1 second. Teleporting them to y<-64 instantly removes most entities.