mirror of
https://github.com/Minestom/Minestom.git
synced 2025-01-01 14:07:43 +01:00
EntityProjectile fixes and optimizations (#807)
This commit is contained in:
parent
cf1373396b
commit
b3ee3e2345
@ -578,13 +578,17 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
|
||||
}
|
||||
|
||||
// World border collision
|
||||
final var finalVelocityPosition = CollisionUtils.applyWorldBorder(instance, position, newPosition);
|
||||
if (finalVelocityPosition.samePoint(position)) {
|
||||
this.velocity = Vec.ZERO;
|
||||
if (hasVelocity) {
|
||||
sendPacketToViewers(getVelocityPacket());
|
||||
final Pos finalVelocityPosition = CollisionUtils.applyWorldBorder(instance, position, newPosition);
|
||||
final boolean positionChanged = !finalVelocityPosition.samePoint(position);
|
||||
if (!positionChanged) {
|
||||
if (!hasVelocity && newVelocity.isZero()) {
|
||||
return;
|
||||
}
|
||||
if (hasVelocity) {
|
||||
this.velocity = Vec.ZERO;
|
||||
sendPacketToViewers(getVelocityPacket());
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
final Chunk finalChunk = ChunkUtils.retrieve(instance, currentChunk, finalVelocityPosition);
|
||||
if (!ChunkUtils.isLoaded(finalChunk)) {
|
||||
@ -592,13 +596,15 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
|
||||
return;
|
||||
}
|
||||
|
||||
if (entityType == EntityTypes.ITEM || entityType == EntityType.FALLING_BLOCK) {
|
||||
// TODO find other exceptions
|
||||
this.previousPosition = this.position;
|
||||
this.position = finalVelocityPosition;
|
||||
refreshCoordinate(finalVelocityPosition);
|
||||
} else {
|
||||
refreshPosition(finalVelocityPosition, true);
|
||||
if (positionChanged) {
|
||||
if (entityType == EntityTypes.ITEM || entityType == EntityType.FALLING_BLOCK) {
|
||||
// TODO find other exceptions
|
||||
this.previousPosition = this.position;
|
||||
this.position = finalVelocityPosition;
|
||||
refreshCoordinate(finalVelocityPosition);
|
||||
} else {
|
||||
refreshPosition(finalVelocityPosition, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Update velocity
|
||||
|
@ -1,15 +1,18 @@
|
||||
package net.minestom.server.entity;
|
||||
|
||||
import net.minestom.server.entity.metadata.ProjectileMeta;
|
||||
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.collision.BoundingBox;
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
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.Nullable;
|
||||
|
||||
@ -18,6 +21,7 @@ import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Class that allows to instantiate entities with projectile-like physics handling.
|
||||
@ -44,30 +48,14 @@ public class EntityProjectile extends Entity {
|
||||
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) {
|
||||
EntityShootEvent shootEvent = new EntityShootEvent(this.shooter, this, to, power, spread);
|
||||
final EntityShootEvent shootEvent = new EntityShootEvent(this.shooter, this, to, power, spread);
|
||||
EventDispatcher.call(shootEvent);
|
||||
if (shootEvent.isCancelled()) {
|
||||
remove();
|
||||
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());
|
||||
}
|
||||
|
||||
@ -75,10 +63,10 @@ public class EntityProjectile extends Entity {
|
||||
double dx = to.x() - from.x();
|
||||
double dy = to.y() - from.y();
|
||||
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;
|
||||
|
||||
double length = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
final double length = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
dx /= length;
|
||||
dy /= length;
|
||||
dz /= length;
|
||||
@ -98,9 +86,9 @@ public class EntityProjectile extends Entity {
|
||||
|
||||
@Override
|
||||
public void tick(long time) {
|
||||
final var posBefore = getPosition();
|
||||
final Pos posBefore = getPosition();
|
||||
super.tick(time);
|
||||
final var posNow = getPosition();
|
||||
final Pos posNow = getPosition();
|
||||
if (isStuck(posBefore, posNow)) {
|
||||
if (super.onGround) {
|
||||
return;
|
||||
@ -109,14 +97,13 @@ public class EntityProjectile extends Entity {
|
||||
this.velocity = Vec.ZERO;
|
||||
sendPacketToViewersAndSelf(getVelocityPacket());
|
||||
setNoGravity(true);
|
||||
onStuck();
|
||||
} else {
|
||||
if (!super.onGround) {
|
||||
return;
|
||||
}
|
||||
super.onGround = false;
|
||||
setNoGravity(false);
|
||||
onUnstuck();
|
||||
EventDispatcher.call(new ProjectileUncollideEvent(this));
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,60 +116,66 @@ public class EntityProjectile extends Entity {
|
||||
*/
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private boolean isStuck(Pos pos, Pos posNow) {
|
||||
final Instance instance = getInstance();
|
||||
if (pos.samePoint(posNow)) {
|
||||
return true;
|
||||
return instance.getBlock(pos).isSolid();
|
||||
}
|
||||
|
||||
Instance instance = getInstance();
|
||||
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.
|
||||
*/
|
||||
double part = .25D; // half of the bounding box
|
||||
final var dir = posNow.sub(pos).asVec();
|
||||
int parts = (int) Math.ceil(dir.length() / part);
|
||||
final var direction = dir.normalize().mul(part).asPosition();
|
||||
final double part = bb.width() / 2;
|
||||
final Vec dir = posNow.sub(pos).asVec();
|
||||
final int parts = (int) Math.ceil(dir.length() / part);
|
||||
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) {
|
||||
// If we're at last part, we can't just add another direction-vector, because we can exceed end point.
|
||||
if (i == parts - 1) {
|
||||
pos = posNow;
|
||||
} else {
|
||||
pos = pos.add(direction);
|
||||
// If we're at last part, we can't just add another direction-vector, because we can exceed the end point.
|
||||
pos = (i == parts - 1) ? posNow : pos.add(direction);
|
||||
if (block == null || !pos.sameBlock(blockPos)) {
|
||||
block = instance.getBlock(pos);
|
||||
blockPos = pos;
|
||||
}
|
||||
Block block = instance.getBlock(pos);
|
||||
if (!block.isAir() && !block.isLiquid()) {
|
||||
teleport(pos);
|
||||
return true;
|
||||
if (block.isSolid()) {
|
||||
final ProjectileCollideWithBlockEvent event = new ProjectileCollideWithBlockEvent(this, pos, block);
|
||||
EventDispatcher.call(event);
|
||||
if (!event.isCancelled()) {
|
||||
teleport(pos);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Chunk currentChunk = instance.getChunkAt(pos);
|
||||
if (currentChunk != chunk) {
|
||||
chunk = currentChunk;
|
||||
entities = instance.getChunkEntities(chunk)
|
||||
.stream()
|
||||
.filter(entity -> entity instanceof LivingEntity)
|
||||
.map(entity -> (LivingEntity) entity)
|
||||
.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
|
||||
shooter and will immediately damage him.
|
||||
We won't check collisions with a shooter for first ticks of arrow's life, because it spawns in him
|
||||
and will immediately deal damage.
|
||||
*/
|
||||
if (getAliveTicks() < 3) {
|
||||
continue;
|
||||
if (aliveTicks < 3 && shooter != null) {
|
||||
victimsStream = victimsStream.filter(entity -> entity != shooter);
|
||||
}
|
||||
Optional<Entity> victimOptional = entities.stream()
|
||||
.filter(entity -> getBoundingBox().intersectEntity(getPosition(), entity))
|
||||
.findAny();
|
||||
final Optional<LivingEntity> victimOptional = victimsStream.findAny();
|
||||
if (victimOptional.isPresent()) {
|
||||
LivingEntity victim = (LivingEntity) victimOptional.get();
|
||||
if(entityType == EntityTypes.ARROW || entityType == EntityTypes.SPECTRAL_ARROW) {
|
||||
victim.setArrowCount(victim.getArrowCount() + 1);
|
||||
final LivingEntity target = victimOptional.get();
|
||||
final ProjectileCollideWithEntityEvent event = new ProjectileCollideWithEntityEvent(this, pos, target);
|
||||
EventDispatcher.call(event);
|
||||
if (!event.isCancelled()) {
|
||||
return super.onGround;
|
||||
}
|
||||
EventDispatcher.call(new EntityAttackEvent(this, victim));
|
||||
remove();
|
||||
return super.onGround;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user