This is a compilation of a bunch of knowledge about minecraft TNT and related mechanics.
Our previous resources gist, made by Pwouik
Xcom's 3-part explanation series explains most of the basics of pearls, tnt, and chunkloading needed for cannons (accurate to mc1.12):
Two old Xcom videos explaining some of the basic concepts behind cannon:
Myren Eario's explanation of TNT duplication:
Methodzz's explanation of TNT movement (accurate to mc1.12):
Xcom's video on how to program an existing ender pearl cannon:
These are all floats
- Fireball: 1
- Wither Skull: 1
- Creeper: 3
- TNT: 4
- Bed: 5
- Respawn Anchor: 5
- Charged Creeper: 6
- End Crystal: 6
- Wither spawning: 7
- TNT Minecart:
4.0 + random.nextDouble() * 1.5 * Math.min(5.0, Math.sqrt(getDeltaMovement().lengthSqr()))
(ranges from 4.0 to 11.5)
Exposure [source]
Exposure [source]
public static float getSeenPercent(Vec3 explosionPos, Entity entity)
{
AABB aabb = entity.getBoundingBox();
// Get the step size for all 3 dimenions
// Will be used to iterate over a 3d grid of points in the entity
double dx = 1.0 / ((aabb.maxX - aabb.minX) * 2.0 + 1.0);
double dy = 1.0 / ((aabb.maxY - aabb.minY) * 2.0 + 1.0);
double dz = 1.0 / ((aabb.maxZ - aabb.minZ) * 2.0 + 1.0);
// Get the X and Z offset from the corner
// Note that there is no Y offset, so the grid is always aligned to the bottom of the entity
// Note that these are calculated wrong which makes the grid be directional, see https://bugs.mojang.com/browse/MC-232355
double xOffset = (1.0 - Math.floor(1.0 / dx) * dx) / 2.0;
double zOffset = (1.0 - Math.floor(1.0 / dz) * dz) / 2.0;
// Return 0 if any of the step sizes is negative
if(dx < 0.0 || dy < 0.0 || dz < 0.0)
return 0F;
int hitRays = 0;
int totalRays = 0;
for(double x = 0.0; x <= 1.0; x += dx)
for(double y = 0.0; y <= 1.0; y += dy)
for(double z = 0.0; z <= 1.0; z += dz)
{
// Calculate the position within the entity based on the current grid position
Vec3 pos = new Vec3(
Mth.lerp(x, aabb.minX, aabb.maxX) + xOffset,
Mth.lerp(y, aabb.minY, aabb.maxY),
Mth.lerp(z, aabb.minZ, aabb.maxZ) + zOffset);
// If there are no blocks obstructing the ray from the pos to the explosion pos, increment hitRays
if(entity.level().clip(new ClipContext(pos, explosionPos, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, entity)).getType() == HitResult.Type.MISS)
++hitRays;
// Always increment totalRays
++totalRays;
}
// Return the ration between hitRays and totalRays (the seen percent)
return (float)hitRays / (float)totalRays;
}
Block breaking [source]
Block breaking [source]
public void explode()
{
this.level.gameEvent(this.source, GameEvent.EXPLODE, new Vec3(this.x, this.y, this.z));
HashSet<BlockPos> blocks = Sets.newHashSet();
// Iterate over a 16x16x16 cube
for(int x = 0; x < 16; ++x)
for(int y = 0; y < 16; ++y)
for(int z = 0; z < 16; ++z)
// Check ifthe point is on the outside faces of the cube
if(x == 0 || x == 15 || y == 0 || y == 15 || z == 0 || z == 15)
{
// Convert point into a direction vector, from the explosion to the outside of the cube, with length 0.3F
double dx = (double)((float)x / 15.0F * 2.0F - 1.0F);
double dy = (double)((float)y / 15.0F * 2.0F - 1.0F);
double dz = (double)((float)z / 15.0F * 2.0F - 1.0F);
double magnitude = Math.sqrt(dx * dx + dy * dy + dz * dz);
dx = dx / magnitude * 0.3F;
dy = dy / magnitude * 0.3F;
dz = dz / magnitude * 0.3F;
// Random.nextFloat is uniformly distributed from 0 to 1
float rayStrength = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F);
// March from the explosion position outwards following the direction vector
double rayX = this.x;
double rayY = this.y;
double rayZ = this.z;
for(; rayStrength > 0.0F; rayStrength -= 0.22500001F)
{
// Get the block at the current ray position, ignoring block shape
BlockPos blockPos = BlockPos.containing(rayX, rayY, rayZ);
BlockState blockState = this.level.getBlockState(blockPos);
FluidState fluidState = this.level.getFluidState(blockPos);
if(!this.level.isInWorldBounds(blockPos))
break;
// See net.minecraft.class_5362.method_29555
Optional blastResistance = this.damageCalculator.getBlockExplosionResistance(this, this.level, blockPos, blockState, fluidState);
// ifthe block has blast resistance, reduce the ray strength accordingly
if(blastResistance.isPresent())
rayStrength -= (blastResistance.get() + 0.3F) * 0.3F;
// ifthe ray strength is still above 0 after reducing, mark the block for deletion
if(rayStrength > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockPos, blockState, rayStrength))
blocks.add(blockPos);
// Advance the ray
rayX += dx;
rayY += dy;
rayZ += dz;
}
}
// Add marked blocks into the toBlow list
// Note: this list will get shuffled before the blocks are actually broken
this.toBlow.addAll(var1);
/* Entity pushing happens here */
}
Entity pushing [source]
Entity pushing [source]
public void explode()
{
/* Block breaking happens here */
// Explosion power is double the radius.
// Radius =
// Fireball, wither skull: 1
// Creeper: 3
// TNT: 4
// Bed: 5
// Respawn anchor: 5
// Charged creeper: 6
// Wither spawning: 7
// TNT minecart: see net.minecraft.class_1701.method_47305
float power = this.radius * 2.0F;
// Get entities around explosion
int minX = Mth.floor(this.x - (double)power - 1.0);
int maxX = Mth.floor(this.x + (double)power + 1.0);
int minY = Mth.floor(this.y - (double)power - 1.0);
int maxY = Mth.floor(this.y + (double)power + 1.0);
int minZ = Mth.floor(this.z - (double)power - 1.0);
int maxZ = Mth.floor(this.z + (double)power + 1.0);
List<Entity> entities = this.level.getEntities(this.source, new AABB((double)minX, (double)minY, (double)minZ, (double)maxX, (double)maxY, (double)maxZ));
Vec3 pos = new Vec3(this.x, this.y, this.z);
for(Entity entity : entities)
{
if(entity.ignoreExplosion())
continue;
// Distance is measured from explosion to entity feet and divided by power
double distance = Math.sqrt(entity.distanceToSqr(pos)) / (double)power;
// ifentity is further than power, skip this entity
if(distance > 1.0)
continue;
// Direction is measured from explosion entity eyes, unless entity is TNT, in which case it's feet
double directionX = entity.getX() - this.x;
double directionY = (entity instanceof PrimedTnt ? entity.getY() : entity.getEyeY()) - this.y;
double directionZ = entity.getZ() - this.z;
double directionMagnitude = Math.sqrt(directionX * directionX + directionY * directionY + directionZ * directionZ);
// ifdirection magnitude is 0, skip this entity
if(directionMagnitude == 0.0)
continue;
// See net.minecraft.class_1927.method_17752
// Will always be 1F ifthere are no blocks between explosion and entity
double exposure = (double)getSeenPercent(pos, entity);
// Velocity magnitude is direction * (1 - distance) * exposure
double knockback = (1.0 - distance) * exposure;
entity.hurt(this.getDamageSource(), (float)((int)((knockback * knockback + knockback) / 2.0 * 7.0 * (double)power + 1.0)));
// Explosion protection enchantment reduces knockback, see net.minecraft.class_1900.method_8237
if(entity instanceof LivingEntity livingEntity)
knockback = ProtectionEnchantment.getExplosionKnockbackAfterDampener(livingEntity, knockback);
// Velocity is direction scaled to the size of knockback
Vec3 deltaMovement = new Vec3(
directionX / directionMagnitude * knockback,
directionY / directionMagnitude * knockback,
directionZ / directionMagnitude * knockback);
entity.setDeltaMovement(entity.getDeltaMovement().add(deltaMovement));
// ifthe entity is a player, add it to the list of hit players
if(entity instanceof Player player && !player.isSpectator() && (!player.isCreative() || !player.getAbilities().flying))
this.hitPlayers.put(player, deltaMovement);
}
}
Note: TNT "Eye position" is the same as the feet position for this calculation
Explanation: https://pastebin.com/jFhrviNp
This section lists useful entity move code. Each entity will have the following:
- Serverside movement code from the official Mojang mappings, with added explanation comments.
- A mathematical formula for position and velocity, if applicable. Note that this won't be float accurate.
- A desmos file showing the formula.
Entity [source]
Entity [source]
public void move(MoverType moverType, Vec3 movement)
{
// Skip all collision checks ifentity has noclip
if(this.noPhysics)
{
this.setPos(this.getX() + movement.x, this.getY() + movement.y, this.getZ() + movement.z);
return;
}
this.wasOnFire = this.isOnFire();
// Adjust movement with piston limiter, then early exit ifthe limited movement is zero
if(moverType == MoverType.PISTON && (movement = this.limitPistonMovement(movement)).equals(Vec3.ZERO))
return;
// stuckSpeedMultiplier gets set by cobwebs, powder snow, and berry bushes. Movement gets multiplied by the multiplier, then velocity is set to zero.
// note: this also gets called by piston movement, which means you can adjust piston movement using different multipliers, and cancel velocity.
if(this.stuckSpeedMultiplier.lengthSqr() > 1.0E-7)
{
movement = movement.multiply(this.stuckSpeedMultiplier);
this.stuckSpeedMultiplier = Vec3.ZERO;
this.setDeltaMovement(Vec3.ZERO);
}
// Mojang wrote this in a hard to read way;
// First, movement is adjusted by maybeBackOffFromEdge.
// Second, movementAdjusted is set to the movement after calculating block and hard entity collisions.
// Third, lengthSqrAdjusted is set to the squared euclidean length of movementAdjusted.
// Fourth, the ifchecks for lengthSqrAdjusted being larger than 1.0E-7.
if((lengthSqrAdjusted = (movementAdjusted = this.collide(movement = this.maybeBackOffFromEdge(movement, moverType))).lengthSqr()) > 1.0E-7)
{
// First, check fallDistance != 0.
// ifthat returned true, check lengthSqrAdjusted >= 1.
// ifthat returned true, perform a raycast from this.position() to this.position() + movementAdjusted.
// The raycast will be stopped early ifa block with the tag FALLDAMAGE_RESETTING or water is hit.
// ifthe raycast gets stopped early, call this.resetFallDistance().
// Note: this raycast loads chunks, causes lag, and could get called for any entity that calls Entity.move().
// Placing FALLDAMAGE_RESETTING blocks or water in the path of big movements can help with the lag.
if(this.fallDistance != 0.0f && lengthSqrAdjusted >= 1.0 && this.level.clip(new ClipContext(this.position(), this.position().add(movementAdjusted), ClipContext.Block.FALLDAMAGE_RESETTING, ClipContext.Fluid.WATER, this)).getType() != HitResult.Type.MISS)
this.resetFallDistance();
// Set position to this.position() + movementAdjusted
this.setPos(this.getX() + movementAdjusted.x, this.getY() + movementAdjusted.y, this.getZ() + movementAdjusted.z);
}
boolean collidedX = !Mth.equal(movement.x, movementAdjusted.x);
boolean collidedZ = !Mth.equal(movement.z, movementAdjusted.z);
this.horizontalCollision = collidedX || collidedZ;
this.verticalCollision = movement.y != movementAdjusted.y;
this.verticalCollisionBelow = this.verticalCollision && movement.y < 0.0;
this.minorHorizontalCollision = this.horizontalCollision ? this.isHorizontalCollisionMinor(movementAdjusted) : false;
this.onGround = this.verticalCollisionBelow;
BlockPos blockPos = this.getOnPos();
BlockState blockState2 = this.level.getBlockState(blockPos);
// Apply fall damage, this can kill the entity.
this.checkFallDamage(movementAdjusted.y, this.onGround, blockState2, blockPos);
// ifit died, early exit.
if(this.isRemoved())
return;
// Cancel velocity on X and/or Z
if(this.horizontalCollision)
{
Vec3 velocity = this.getDeltaMovement();
this.setDeltaMovement(collidedX ? 0.0 : velocity.x, velocity.y, collidedZ ? 0.0 : velocity.z);
}
Block block = blockState2.getBlock();
if(this.verticalCollision)
block.updateEntityAfterFallOn(this.level, this);
if(this.onGround && !this.isSteppingCarefully())
block.stepOn(this.level, blockPos, blockState2, this);
this.tryCheckInsideBlocks();
// Get block friction
float j = this.getBlockSpeedFactor();
// Apply block friction
this.setDeltaMovement(this.getDeltaMovement().multiply(j, 1.0, j));
if(this.level.getBlockStatesIfLoaded(this.getBoundingBox().deflate(1.0E-6)).noneMatch(blockState -> blockState.is(BlockTags.FIRE) || blockState.is(Blocks.LAVA))) {
if(this.remainingFireTicks <= 0) {
this.setRemainingFireTicks(-this.getFireImmuneTicks());
}
if(this.wasOnFire && (this.isInPowderSnow || this.isInWaterRainOrBubble())) {
this.playEntityOnFireExtinguishedSound();
}
}
if(this.isOnFire() && (this.isInPowderSnow || this.isInWaterRainOrBubble())) {
this.setRemainingFireTicks(-this.getFireImmuneTicks());
}
}
TNT [source]
TNT [source]
Code
public void tick()
{
// ifTNT has NoGravity NBT tag set to 0, add [0D, -0.04D, 0D] to its motion
if(!this.isNoGravity())
this.setDeltaMovement(this.getDeltaMovement().add(0.0, -0.04, 0.0));
// Call the Entity.move method
this.move(MoverType.SELF, this.getDeltaMovement());
// Multiply motion by 0.98D
this.setDeltaMovement(this.getDeltaMovement().scale(0.98));
// ifTNT is on ground (AKA ended Entity.move on ground), multiply motion by [0.7D, -0.5D, 0.7D]
if(this.onGround)
this.setDeltaMovement(this.getDeltaMovement().multiply(0.7, -0.5, 0.7));
// Decrement fuse timer
int i = this.getFuse() - 1;
this.setFuse(i);
// iffuse timer is smaller or equal to 0, remove entity and explode
if(i <= 0)
{
this.discard();
this.explode();
}
else
this.updateInWaterStateAndDoFluidPushing();
}
Formula
https://www.desmos.com/calculator/r33a6enfn5
Code
public void tick()
{
// Call the Projectile.tick method
super.tick();
// Perform a raycast that hits block collision boxes and entities
// Raycast extends from entity feet to entity feet + deltaMovement
// See net.minecraft.class_1675.method_49997 for details
// Some entities are skipped: spectators, Projectiles, entities in the same vehicle as the entity that threw the projectile, etc:
// See net.minecraft.class_1676.method_26958 for details
HitResult hitResult = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity);
boolean handled = false;
if(hitResult.getType() == HitResult.Type.BLOCK)
{
// Get the block hit by the raycast
BlockPos blockPos = ((BlockHitResult)hitResult).getBlockPos();
BlockState blockState = this.level().getBlockState(blockPos);
// ifblock is a nether portal, set entity inside portal. See net.minecraft.class_1297.method_5717 for details.
if(blockState.is(Blocks.NETHER_PORtal))
{
this.handleInsidePortal(blockPos);
handled = true;
}
// ifblock is an end gateway, attempt to teleport
else if(blockState.is(Blocks.END_GATEWAY))
{
BlockEntity blockEntity = this.level().getBlockEntity(blockPos);
if(blockEntity instanceof TheEndGatewayBlockEntity endGatewayBlockEntity && TheEndGatewayBlockEntity.canEntityTeleport(this))
TheEndGatewayBlockEntity.teleportEntity(this.level(), blockPos, blockState, this, endGatewayBlockEntity);
handled = true;
}
}
// ifthe raycast didn't miss and it hasn't already been handled, execute this.onHit.
// For ender pearls it will teleport, for snowballs, it'll damage, for eggs it'll try spawning chickens, for potions it'll break the potion.
if(hitResult.getType() != HitResult.Type.MISS && !handled)
this.onHit(hitResult);
// See net.minecraft.class_1297.method_5852
this.checkInsideBlocks();
// Calculate new pos
Vec3 deltaMovement = this.getDeltaMovement();
double x = this.getX() + deltaMovement.x;
double y = this.getY() + deltaMovement.y;
double z = this.getZ() + deltaMovement.z;
this.updateRotation();
// Apply drag
float drag = this.isInWater() ? 0.8f : 0.99f;
this.setDeltaMovement(deltaMovement.scale((double)drag));
// Apply gravity. this.getGravity() is 0.03F for all ThrowableProjectiles except potions which are 0.05F and xp bottles which are 0.07F
if(!this.isNoGravity())
{
deltaMovement = this.getDeltaMovement();
this.setDeltaMovement(deltaMovement.x, deltaMovement.y - (double)this.getGravity(), deltaMovement.z);
}
// Apply new pos
this.setPos(x, y, z);
}
Formula
https://www.desmos.com/calculator/s2lwhidtgt