EntityProjectile fixes and optimizations (#807)

This commit is contained in:
Konstantin Shandurenko 2022-03-28 23:06:25 +03:00 committed by GitHub
parent cf1373396b
commit b3ee3e2345
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 319 additions and 75 deletions

View File

@ -578,20 +578,25 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
} }
// World border collision // World border collision
final var finalVelocityPosition = CollisionUtils.applyWorldBorder(instance, position, newPosition); final Pos finalVelocityPosition = CollisionUtils.applyWorldBorder(instance, position, newPosition);
if (finalVelocityPosition.samePoint(position)) { final boolean positionChanged = !finalVelocityPosition.samePoint(position);
this.velocity = Vec.ZERO; if (!positionChanged) {
if (hasVelocity) { if (!hasVelocity && newVelocity.isZero()) {
sendPacketToViewers(getVelocityPacket());
}
return; return;
} }
if (hasVelocity) {
this.velocity = Vec.ZERO;
sendPacketToViewers(getVelocityPacket());
return;
}
}
final Chunk finalChunk = ChunkUtils.retrieve(instance, currentChunk, finalVelocityPosition); final Chunk finalChunk = ChunkUtils.retrieve(instance, currentChunk, finalVelocityPosition);
if (!ChunkUtils.isLoaded(finalChunk)) { if (!ChunkUtils.isLoaded(finalChunk)) {
// Entity shouldn't be updated when moving in an unloaded chunk // Entity shouldn't be updated when moving in an unloaded chunk
return; return;
} }
if (positionChanged) {
if (entityType == EntityTypes.ITEM || entityType == EntityType.FALLING_BLOCK) { if (entityType == EntityTypes.ITEM || entityType == EntityType.FALLING_BLOCK) {
// TODO find other exceptions // TODO find other exceptions
this.previousPosition = this.position; this.previousPosition = this.position;
@ -600,6 +605,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
} else { } else {
refreshPosition(finalVelocityPosition, true); refreshPosition(finalVelocityPosition, true);
} }
}
// Update velocity // Update velocity
if (hasVelocity || !newVelocity.isZero()) { if (hasVelocity || !newVelocity.isZero()) {

View File

@ -1,15 +1,18 @@
package net.minestom.server.entity; package net.minestom.server.entity;
import net.minestom.server.entity.metadata.ProjectileMeta; import net.minestom.server.collision.BoundingBox;
import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.entity.EntityAttackEvent;
import net.minestom.server.event.entity.EntityShootEvent;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec; import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.metadata.ProjectileMeta;
import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.entity.EntityShootEvent;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent;
import net.minestom.server.event.entity.projectile.ProjectileUncollideEvent;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -18,6 +21,7 @@ import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
/** /**
* Class that allows to instantiate entities with projectile-like physics handling. * Class that allows to instantiate entities with projectile-like physics handling.
@ -44,30 +48,14 @@ public class EntityProjectile extends Entity {
return this.shooter; return this.shooter;
} }
/**
* Called when this projectile is stuck in blocks.
* Probably you want to do nothing with arrows in such case and to remove other types of projectiles.
*/
public void onStuck() {
}
/**
* Called when this projectile unstucks.
* Probably you want to add some random velocity to arrows in such case.
*/
public void onUnstuck() {
}
public void shoot(Point to, double power, double spread) { public void shoot(Point to, double power, double spread) {
EntityShootEvent shootEvent = new EntityShootEvent(this.shooter, this, to, power, spread); final EntityShootEvent shootEvent = new EntityShootEvent(this.shooter, this, to, power, spread);
EventDispatcher.call(shootEvent); EventDispatcher.call(shootEvent);
if (shootEvent.isCancelled()) { if (shootEvent.isCancelled()) {
remove(); remove();
return; return;
} }
final var from = this.shooter.getPosition().add(0D, this.shooter.getEyeHeight(), 0D); final Pos from = this.shooter.getPosition().add(0D, this.shooter.getEyeHeight(), 0D);
shoot(from, to, shootEvent.getPower(), shootEvent.getSpread()); shoot(from, to, shootEvent.getPower(), shootEvent.getSpread());
} }
@ -75,10 +63,10 @@ public class EntityProjectile extends Entity {
double dx = to.x() - from.x(); double dx = to.x() - from.x();
double dy = to.y() - from.y(); double dy = to.y() - from.y();
double dz = to.z() - from.z(); double dz = to.z() - from.z();
double xzLength = Math.sqrt(dx * dx + dz * dz); final double xzLength = Math.sqrt(dx * dx + dz * dz);
dy += xzLength * 0.20000000298023224D; dy += xzLength * 0.20000000298023224D;
double length = Math.sqrt(dx * dx + dy * dy + dz * dz); final double length = Math.sqrt(dx * dx + dy * dy + dz * dz);
dx /= length; dx /= length;
dy /= length; dy /= length;
dz /= length; dz /= length;
@ -98,9 +86,9 @@ public class EntityProjectile extends Entity {
@Override @Override
public void tick(long time) { public void tick(long time) {
final var posBefore = getPosition(); final Pos posBefore = getPosition();
super.tick(time); super.tick(time);
final var posNow = getPosition(); final Pos posNow = getPosition();
if (isStuck(posBefore, posNow)) { if (isStuck(posBefore, posNow)) {
if (super.onGround) { if (super.onGround) {
return; return;
@ -109,14 +97,13 @@ public class EntityProjectile extends Entity {
this.velocity = Vec.ZERO; this.velocity = Vec.ZERO;
sendPacketToViewersAndSelf(getVelocityPacket()); sendPacketToViewersAndSelf(getVelocityPacket());
setNoGravity(true); setNoGravity(true);
onStuck();
} else { } else {
if (!super.onGround) { if (!super.onGround) {
return; return;
} }
super.onGround = false; super.onGround = false;
setNoGravity(false); setNoGravity(false);
onUnstuck(); EventDispatcher.call(new ProjectileUncollideEvent(this));
} }
} }
@ -129,62 +116,68 @@ public class EntityProjectile extends Entity {
*/ */
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
private boolean isStuck(Pos pos, Pos posNow) { private boolean isStuck(Pos pos, Pos posNow) {
final Instance instance = getInstance();
if (pos.samePoint(posNow)) { if (pos.samePoint(posNow)) {
return true; return instance.getBlock(pos).isSolid();
} }
Instance instance = getInstance();
Chunk chunk = null; Chunk chunk = null;
Collection<Entity> entities = null; Collection<LivingEntity> entities = null;
final BoundingBox bb = getBoundingBox();
/* /*
What we're about to do is to discretely jump from the previous position to the new one. What we're about to do is to discretely jump from a previous position to the new one.
For each point we will be checking blocks and entities we're in. For each point we will be checking blocks and entities we're in.
*/ */
double part = .25D; // half of the bounding box final double part = bb.width() / 2;
final var dir = posNow.sub(pos).asVec(); final Vec dir = posNow.sub(pos).asVec();
int parts = (int) Math.ceil(dir.length() / part); final int parts = (int) Math.ceil(dir.length() / part);
final var direction = dir.normalize().mul(part).asPosition(); final Pos direction = dir.normalize().mul(part).asPosition();
final long aliveTicks = getAliveTicks();
Block block = null;
Point blockPos = null;
for (int i = 0; i < parts; ++i) { for (int i = 0; i < parts; ++i) {
// If we're at last part, we can't just add another direction-vector, because we can exceed end point. // If we're at last part, we can't just add another direction-vector, because we can exceed the end point.
if (i == parts - 1) { pos = (i == parts - 1) ? posNow : pos.add(direction);
pos = posNow; if (block == null || !pos.sameBlock(blockPos)) {
} else { block = instance.getBlock(pos);
pos = pos.add(direction); blockPos = pos;
} }
Block block = instance.getBlock(pos); if (block.isSolid()) {
if (!block.isAir() && !block.isLiquid()) { final ProjectileCollideWithBlockEvent event = new ProjectileCollideWithBlockEvent(this, pos, block);
EventDispatcher.call(event);
if (!event.isCancelled()) {
teleport(pos); teleport(pos);
return true; return true;
} }
Chunk currentChunk = instance.getChunkAt(pos); }
if (currentChunk != chunk) { if (currentChunk != chunk) {
chunk = currentChunk; chunk = currentChunk;
entities = instance.getChunkEntities(chunk) entities = instance.getChunkEntities(chunk)
.stream() .stream()
.filter(entity -> entity instanceof LivingEntity) .filter(entity -> entity instanceof LivingEntity)
.map(entity -> (LivingEntity) entity)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
Stream<LivingEntity> victimsStream = entities.stream()
.filter(entity -> bb.intersectEntity(getPosition(), entity));
/* /*
We won't check collisions with entities for first ticks of arrow's life, because it spawns in the We won't check collisions with a shooter for first ticks of arrow's life, because it spawns in him
shooter and will immediately damage him. and will immediately deal damage.
*/ */
if (getAliveTicks() < 3) { if (aliveTicks < 3 && shooter != null) {
continue; victimsStream = victimsStream.filter(entity -> entity != shooter);
} }
Optional<Entity> victimOptional = entities.stream() final Optional<LivingEntity> victimOptional = victimsStream.findAny();
.filter(entity -> getBoundingBox().intersectEntity(getPosition(), entity))
.findAny();
if (victimOptional.isPresent()) { if (victimOptional.isPresent()) {
LivingEntity victim = (LivingEntity) victimOptional.get(); final LivingEntity target = victimOptional.get();
if(entityType == EntityTypes.ARROW || entityType == EntityTypes.SPECTRAL_ARROW) { final ProjectileCollideWithEntityEvent event = new ProjectileCollideWithEntityEvent(this, pos, target);
victim.setArrowCount(victim.getArrowCount() + 1); EventDispatcher.call(event);
} if (!event.isCancelled()) {
EventDispatcher.call(new EntityAttackEvent(this, victim));
remove();
return super.onGround; return super.onGround;
} }
} }
}
return false; return false;
} }
} }

View File

@ -0,0 +1,39 @@
package net.minestom.server.event.entity.projectile;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity;
import net.minestom.server.event.trait.CancellableEvent;
import net.minestom.server.event.trait.EntityInstanceEvent;
import net.minestom.server.event.trait.RecursiveEvent;
import org.jetbrains.annotations.NotNull;
class ProjectileCollideEvent implements EntityInstanceEvent, CancellableEvent, RecursiveEvent {
private final @NotNull Entity projectile;
private final @NotNull Pos position;
private boolean cancelled;
protected ProjectileCollideEvent(@NotNull Entity projectile, @NotNull Pos position) {
this.projectile = projectile;
this.position = position;
}
@Override
public @NotNull Entity getEntity() {
return projectile;
}
public @NotNull Pos getCollisionPosition() {
return position;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancel) {
cancelled = cancel;
}
}

View File

@ -0,0 +1,24 @@
package net.minestom.server.event.entity.projectile;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity;
import net.minestom.server.instance.block.Block;
import org.jetbrains.annotations.NotNull;
public final class ProjectileCollideWithBlockEvent extends ProjectileCollideEvent {
private final @NotNull Block block;
public ProjectileCollideWithBlockEvent(
@NotNull Entity projectile,
@NotNull Pos position,
@NotNull Block block
) {
super(projectile, position);
this.block = block;
}
public @NotNull Block getBlock() {
return block;
}
}

View File

@ -0,0 +1,23 @@
package net.minestom.server.event.entity.projectile;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity;
import org.jetbrains.annotations.NotNull;
public final class ProjectileCollideWithEntityEvent extends ProjectileCollideEvent {
private final @NotNull Entity target;
public ProjectileCollideWithEntityEvent(
@NotNull Entity projectile,
@NotNull Pos position,
@NotNull Entity target
) {
super(projectile, position);
this.target = target;
}
public @NotNull Entity getTarget() {
return target;
}
}

View File

@ -0,0 +1,20 @@
package net.minestom.server.event.entity.projectile;
import net.minestom.server.entity.Entity;
import net.minestom.server.event.trait.EntityInstanceEvent;
import org.jetbrains.annotations.NotNull;
public final class ProjectileUncollideEvent implements EntityInstanceEvent {
private final @NotNull Entity projectile;
public ProjectileUncollideEvent(@NotNull Entity projectile) {
this.projectile = projectile;
}
@Override
public @NotNull Entity getEntity() {
return projectile;
}
}

View File

@ -0,0 +1,139 @@
package net.minestom.server.collision;
import net.minestom.server.MinecraftServer;
import net.minestom.server.api.Env;
import net.minestom.server.api.EnvTest;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityProjectile;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.LivingEntity;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent;
import net.minestom.server.event.entity.projectile.ProjectileUncollideEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import net.minestom.server.utils.time.TimeUnit;
import org.junit.jupiter.api.Test;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
@EnvTest
public class EntityProjectileCollisionIntegrationTest {
@Test
public void blockShootAndBlockRemoval(Env env) {
final Instance instance = env.createFlatInstance();
instance.getWorldBorder().setDiameter(1000.0);
final Entity shooter = new Entity(EntityType.SKELETON);
shooter.setInstance(instance, new Pos(0, 40, 0)).join();
final EntityProjectile projectile = new EntityProjectile(shooter, EntityType.ARROW);
projectile.setInstance(instance, shooter.getPosition().withY(y -> y + shooter.getEyeHeight())).join();
final Point blockPosition = new Vec(5, 40, 0);
final Block block = Block.GRASS_BLOCK;
instance.setBlock(blockPosition, block);
projectile.shoot(blockPosition, 1, 0);
final var eventRef = new AtomicReference<ProjectileCollideWithBlockEvent>();
MinecraftServer.getGlobalEventHandler().addListener(ProjectileCollideWithBlockEvent.class, eventRef::set);
final long tick = TimeUnit.SERVER_TICK.getDuration().toMillis();
for (int i = 0; i < MinecraftServer.TICK_PER_SECOND; ++i) {
projectile.tick(i * tick);
}
var event = eventRef.get();
assertNotNull(event);
assertEquals(blockPosition, new Vec(event.getCollisionPosition().blockX(), event.getCollisionPosition().blockY(), event.getCollisionPosition().blockZ()));
assertEquals(block, event.getBlock());
final var eventRef2 = new AtomicReference<ProjectileUncollideEvent>();
MinecraftServer.getGlobalEventHandler().addListener(ProjectileUncollideEvent.class, eventRef2::set);
eventRef.set(null);
instance.setBlock(blockPosition, Block.AIR);
for (int i = 0; i < MinecraftServer.TICK_PER_SECOND; ++i) {
projectile.tick((MinecraftServer.TICK_PER_SECOND + i) * tick);
}
event = eventRef.get();
final var event2 = eventRef2.get();
assertNotNull(event);
assertNotNull(event2);
assertEquals(blockPosition.withY(y -> y - 1), new Vec(event.getCollisionPosition().blockX(), event.getCollisionPosition().blockY(), event.getCollisionPosition().blockZ()));
}
@Test
public void entityShoot(Env env) {
final Instance instance = env.createFlatInstance();
instance.getWorldBorder().setDiameter(1000.0);
final Entity shooter = new Entity(EntityType.SKELETON);
shooter.setInstance(instance, new Pos(0, 40, 0)).join();
final EntityProjectile projectile = new EntityProjectile(shooter, EntityType.ARROW);
projectile.setInstance(instance, shooter.getPosition().withY(y -> y + shooter.getEyeHeight())).join();
final Point targetPosition = new Vec(5, 40, 0);
final LivingEntity target = new LivingEntity(EntityType.ZOMBIE);
target.setInstance(instance, Pos.fromPoint(targetPosition)).join();
projectile.shoot(targetPosition, 1, 0);
final var eventRef = new AtomicReference<ProjectileCollideWithEntityEvent>();
MinecraftServer.getGlobalEventHandler().addListener(ProjectileCollideWithEntityEvent.class, event -> {
event.getEntity().remove();
eventRef.set(event);
});
final long tick = TimeUnit.SERVER_TICK.getDuration().toMillis();
for (int i = 0; i < MinecraftServer.TICK_PER_SECOND; ++i) {
if (!projectile.isRemoved()) {
projectile.tick(i * tick);
}
}
final var event = eventRef.get();
assertNotNull(event);
assertSame(target, event.getTarget());
assertTrue(target.getBoundingBox().intersectEntity(target.getPosition(), projectile));
}
@Test
public void entitySelfShoot(Env env) {
final Instance instance = env.createFlatInstance();
instance.getWorldBorder().setDiameter(1000.0);
final LivingEntity shooter = new LivingEntity(EntityType.SKELETON);
shooter.setInstance(instance, new Pos(0, 40, 0)).join();
final EntityProjectile projectile = new EntityProjectile(shooter, EntityType.ARROW);
projectile.setInstance(instance, shooter.getPosition().withY(y -> y + shooter.getEyeHeight())).join();
projectile.shoot(new Vec(0, 60, 0), 1, 0);
final var eventRef = new AtomicReference<ProjectileCollideWithEntityEvent>();
MinecraftServer.getGlobalEventHandler().addListener(ProjectileCollideWithEntityEvent.class, event -> {
event.getEntity().remove();
eventRef.set(event);
});
final long tick = TimeUnit.SERVER_TICK.getDuration().toMillis();
for (int i = 0; i < MinecraftServer.TICK_PER_SECOND * 5; ++i) {
if (!projectile.isRemoved()) {
projectile.tick(i * tick);
}
}
final var event = eventRef.get();
assertNotNull(event);
assertSame(shooter, event.getTarget());
assertTrue(shooter.getBoundingBox().intersectEntity(shooter.getPosition(), projectile));
}
}