package net.kennux.cubicworld.entity;
import net.kennux.cubicworld.CubicWorldConfiguration;
import net.kennux.cubicworld.serialization.BitReader;
import net.kennux.cubicworld.serialization.BitWriter;
import net.kennux.cubicworld.voxel.VoxelWorld;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g3d.ModelBatch;
import com.badlogic.gdx.graphics.g3d.decals.DecalBatch;
import com.badlogic.gdx.math.Vector3;
/**
* <pre>
* The entity manager will hold multiple entities.
* As it's name says, it will manage them.
* So it can:
*
* - Update them
* - Interpolate their position
* - Render them
*
* The update() method currently only iterates overy every entity. If there was
* a player position given, a range check will get performed to check if the
* entity is in the player's entity update distance.
* update() will also call the interpolatePosition() function on every object
* with the interpolation mode given in the constructor (direct or not).
*
* If direct the entity's target position will get immediately flushed to the
* entity position.
* If not direct, a lerp() will be performed.
*
* The render() method calls the render() method on every entity in this entity
* manager.
* </pre>
*
* @author KennuX
*
*/
public class EntityManager
{
/**
* The entity ids. The index in this array maps to the entities array.
*/
private Integer[] entityIds;
/**
* The entities array.
*/
private AEntity[] entities;
/**
* Lock this if you access entityids or entities.
*/
private Object lockObject = new Object();
/**
* If this is set to true, direct interpolation will get used in the
* interpolatePosition() call.
*/
private boolean directPositionInterpolation = false;
/**
* Gets set to false if we are on a server.
* Dont init anything rendering related on the server.
*/
private boolean isServer = false;
/**
* The voxel world set in the constructor.
*/
private VoxelWorld voxelWorld;
private int entityCounter = 0;
/**
* Client constructor.
*/
public EntityManager(VoxelWorld voxelWorld)
{
this.entities = new AEntity[128];
this.entityIds = new Integer[128];
this.voxelWorld = voxelWorld;
}
/**
* Server constructor.
*
* @param useDirectInterpolation
* @param isServer
* @param entityCounterStart
* the Start id for the entity manager (slotcount) on server.
*/
public EntityManager(VoxelWorld voxelWorld, boolean useDirectInterpolation, boolean isServer, int entityCounterStart)
{
this(voxelWorld);
this.directPositionInterpolation = useDirectInterpolation;
this.isServer = isServer;
this.entityCounter = entityCounterStart;
}
/**
* Adds an entity to this entity manager instance.
*
* @param id
* @param entity
*/
public void add(int id, AEntity entity)
{
synchronized (this.lockObject)
{
if (!this.containsId(id))
{
int freeIndex = this.findFreeIndex();
if (freeIndex == -1)
{
this.extend(this.entityIds.length + 128);
this.add(id, entity);
}
// Set id on the entity
entity.setEntityId(id);
this.entities[freeIndex] = entity;
this.entityIds[freeIndex] = new Integer(id);
entity.setMaster(this);
entity.init();
}
}
}
/**
* Performs a cleanup.
* Interates through every entity and checks if it is too far away from players to exist.
*
* @param playerPositions
*/
public void cleanup(Vector3[] playerPositions)
{
synchronized (this.lockObject)
{
for (AEntity entity : this.entities)
{
if (entity != null)
{
for (Vector3 position : playerPositions)
{
if (new Vector3(entity.getPosition()).sub(position).len() > CubicWorldConfiguration.entityCullDistance)
{
entity.die();
}
}
}
}
}
}
/**
* Deletes all entities in the list who haven't got an update since timeout
* milliseconds.
*
* @param timeout
*/
public void cleanupUpdateTimeout(int timeout)
{
for (int i = 0; i < this.entityIds.length; i++)
{
if (this.entities[i] != null && System.currentTimeMillis() - this.entities[i].getLastUpdateTime() >= timeout)
{
this.remove(this.entityIds[i]);
}
}
}
/**
* Returns true if the given id is already in use.
*
* @param id
* @return
*/
public boolean containsId(int id)
{
return this.findIndexById(id) != -1;
}
/**
* Deserializes the entities in the reader's data.
*
* @param reader
*/
public void deserialize(BitReader reader)
{
synchronized (this.lockObject)
{
while (reader.hasDataLeft())
{
// Get entity id
AEntity entity = EntitySystem.instantiateEntity(reader.readInt());
// Deserialize entity data
entity.deserializeInitial(reader);
// Add entity
this.add(this.getNextFreeId(), entity);
}
}
}
/**
* Extends this instance's capacity. newSize is the new capacity.
*
* @param newSize
*/
private void extend(int newSize)
{
// Create new arrays
Integer[] newEntityIds = new Integer[newSize];
AEntity[] newEntities = new AEntity[newSize];
// Copy old data
System.arraycopy(this.entityIds, 0, newEntityIds, 0, this.entityIds.length);
System.arraycopy(this.entities, 0, newEntities, 0, this.entities.length);
// Set new arrays
this.entityIds = newEntityIds;
this.entities = newEntities;
}
/**
* Searches in the keys array for a free index and return's it's index.
* REMEMBER: key and value array indices are the same.
*
* Returns -1 if there was no free index found (extend should get called
* then to extend the arrays).
*
* @return
*/
private int findFreeIndex()
{
for (int i = 0; i < this.entityIds.length; i++)
{
if (this.entityIds[i] == null)
return i;
}
return -1;
}
/**
* Returns the index of the entity to search. Returns -1 if the entity was
* not found.
*
* @param key
* @return
*/
@SuppressWarnings("unused")
private int findIndexByEntity(AEntity entity)
{
for (int i = 0; i < this.entities.length; i++)
{
// Search in the entity index.
if (this.entities[i] != null && this.entities[i] == entity)
{
return i;
}
}
// Not found
return -1;
}
/**
* Returns the index of the key to search. Returns -1 if the key was not
* found.
*
* @param key
* @return
*/
private int findIndexById(Integer id)
{
for (int i = 0; i < this.entityIds.length; i++)
{
// Search in the entity index.
if (this.entityIds[i] != null && this.entityIds[i].equals(id))
{
return i;
}
}
// Not found
return -1;
}
/**
* Returns the entity saved with the given id. Returns null if the entity
* was not found.
*
* @param id
* @return
*/
public AEntity get(int id)
{
synchronized (this.lockObject)
{
// Search index
int index = this.findIndexById(id);
if (index == -1)
return null;
return this.entities[index];
}
}
/**
* Returns all entitys as an array.
*
* @return
*/
public AEntity[] getEntityArray()
{
return this.entities.clone();
}
/**
* Increments the entity counter and returns it (First increment, then
* return so counterstart at 60 means first id is 61).
*
* @return
*/
public int getNextFreeId()
{
this.entityCounter++;
return this.entityCounter;
}
/**
* Used to return a reference of the voxelworld set to this instance.
*
* @return
*/
public VoxelWorld getWorld()
{
return this.voxelWorld;
}
public boolean isClient()
{
return !this.isServer;
}
public boolean isServer()
{
return this.isServer;
}
/**
* Removes the entity. If it is not in the list, nothing will be done here.
* This class does not handle any destroy packet sending.
* If you need immediate the object to be immediately destroyed on the
* client, usethe destroyEntity() function of the CubicWorldServer class.
* Otherwise it will die after some time not recieving updates from the
* server.
*
* @param id
*/
public void remove(int id)
{
synchronized (this.lockObject)
{
int index = this.findIndexById(id);
if (index != -1)
this.removeIndex(index);
}
}
/**
* Removes the entity in the given index (set's the references to null).
*
* @param index
*/
private void removeIndex(int index)
{
this.entities[index] = null;
this.entityIds[index] = null;
}
/**
* Renders all entities managed (added by the add() method).
*/
public void render(Camera camera, ModelBatch modelBatch, DecalBatch decalBatch, SpriteBatch spriteBatch, BitmapFont bitmapFont)
{
synchronized (this.lockObject)
{
for (AEntity entity : this.entities)
{
if (entity != null)
entity.render(camera, modelBatch, decalBatch, spriteBatch, bitmapFont);
}
}
}
/**
* Serializes ALL entities in this entity manager.
*
* @param writer
*/
public void serialize(BitWriter writer)
{
synchronized (this.lockObject)
{
for (AEntity entity : this.entities)
{
// Skip not initialized entities
if (entity == null)
continue;
// Write entity data
writer.writeInt(EntitySystem.reverseLookup(entity.getClass()));
entity.serializeInitial(writer);
}
}
}
/**
* Updates all entities managed (added by the add() method).
* Calls the update method and interpolate position!
*/
public void update()
{
synchronized (this.lockObject)
{
for (AEntity entity : this.entities)
{
if (entity != null)
{
entity.interpolatePosition(this.directPositionInterpolation);
entity.update();
}
}
}
}
/**
* Updates all entities managed (added by the add() method).
* This call also performs distance culling and removes all entities out of
* range.
*/
public void update(Vector3 playerPosition)
{
synchronized (this.lockObject)
{
for (AEntity entity : this.entities)
{
if (entity != null)
{
entity.interpolatePosition(this.directPositionInterpolation);
if (entity.isInEntityViewDistance(playerPosition))
entity.update();
}
}
}
}
}