/* * The MIT License (MIT) * * FXGL - JavaFX Game Library * * Copyright (c) 2015-2017 AlmasB (almaslvl@gmail.com) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.almasb.fxgl.ecs; import com.almasb.fxgl.core.collection.Array; import com.almasb.fxgl.core.collection.ObjectMap; import com.almasb.fxgl.core.logging.FXGLLogger; import com.almasb.fxgl.core.logging.Logger; import com.almasb.fxgl.core.reflect.ReflectionUtils; import com.almasb.fxgl.ecs.component.Required; import com.almasb.fxgl.io.serialization.Bundle; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; import java.util.ArrayList; import java.util.List; import java.util.Optional; /** * A generic entity in the Entity-Component-System (Control) model. * During update (or control update) it is not allowed to: * <ul> * <li>Add control</li> * <li>Remove control</li> * </ul> * * @author Almas Baimagambetov (AlmasB) (almaslvl@gmail.com) */ public class Entity { private static final Logger log = FXGLLogger.get(Entity.class); private final ObjectMap<String, Object> properties = new ObjectMap<>(); ObjectMap<Class<? extends Control>, Control> controls = new ObjectMap<>(); ObjectMap<Class<? extends Component>, Component> components = new ObjectMap<>(); private ReadOnlyBooleanWrapper active = new ReadOnlyBooleanWrapper(false); /** * Set a property specified by a key-value pair. * Prefer {@link Component} instead. * * @param key property key * @param value property value */ public final void setProperty(String key, Object value) { checkValid(); properties.put(key, value); } /** * Retrieve a property value by a given key. * Prefer {@link Component} instead. * * @param key property key * @param <T> value type * @return property value * @throws IllegalArgumentException if key doesn't exist */ @SuppressWarnings("unchecked") public final <T> T getProperty(String key) { checkValid(); Object value = properties.get(key, null); if (value == null) throw new IllegalArgumentException("No property with key: " + key); return (T) value; } /* CONTROL BEGIN */ /** * @param type control type * @return true iff entity has control of given type */ public final boolean hasControl(Class<? extends Control> type) { checkValid(); return controls.containsKey(type); } /** * Returns control of given type or {@link Optional#empty()} if * no such type is registered on this entity. * * @param type control type * @return control */ public final <T extends Control> Optional<T> getControl(Class<T> type) { checkValid(); return Optional.ofNullable(getControlUnsafe(type)); } /** * Returns control of given type or null if no such type is registered. * * @param type control type * @return control */ public final <T extends Control> T getControlUnsafe(Class<T> type) { checkValid(); return type.cast(controls.get(type)); } /** * Warning: object allocation. * Cannot be called during update. * * @return array of controls */ public final Array<Control> getControls() { checkValid(); return controls.values().toArray(); } /** * Adds behavior to entity. * Only 1 control per type is allowed. * Anonymous controls are not allowed. * Cannot add controls within update() of another control. * * @param control the behavior * @throws IllegalArgumentException if control with same type already registered or anonymous * @throws IllegalStateException if components required by the given control are missing */ public final void addControl(Control control) { checkValid(); Class<? extends Control> type = control.getClass(); if (type.getCanonicalName() == null) { log.fatal("Adding anonymous control: " + type.getName()); throw new IllegalArgumentException("Anonymous controls are not allowed! - " + type.getName()); } if (hasControl(type)) { log.fatal("Entity already has a control with type: " + type.getCanonicalName()); throw new IllegalArgumentException("Entity already has a control with type: " + type.getCanonicalName()); } checkRequirementsMet(control.getClass()); controls.put(control.getClass(), control); if (control instanceof AbstractControl) { ((AbstractControl) control).setEntity(this); } injectFields(control); control.onAdded(this); notifyControlAdded(control); } @SuppressWarnings("unchecked") private void injectFields(Control control) { ReflectionUtils.findFieldsByType(control, Component.class).forEach(field -> { Component comp = getComponentUnsafe((Class<? extends Component>) field.getType()); if (comp != null) { ReflectionUtils.inject(field, control, comp); } else { log.warning("Injection failed, entity has no component: " + field.getType()); } }); ReflectionUtils.findFieldsByType(control, Control.class).forEach(field -> { Control ctrl = getControlUnsafe((Class<? extends Control>) field.getType()); if (ctrl != null) { ReflectionUtils.inject(field, control, ctrl); } else { log.warning("Injection failed, entity has no control: " + field.getType()); } }); } /** * @param type the control type to remove */ public final void removeControl(Class<? extends Control> type) { checkValid(); Control control = getControlUnsafe(type); if (control == null) { log.warning("Cannot remove control " + type.getSimpleName() + ". Entity does not have one"); } else { controls.remove(control.getClass()); removeControlImpl(control); } } /** * Remove all controls from entity. */ public final void removeAllControls() { checkValid(); for (Control control : controls.values()) { removeControlImpl(control); } controls.clear(); } private void removeControlImpl(Control control) { notifyControlRemoved(control); control.onRemoved(this); if (control instanceof AbstractControl) { ((AbstractControl) control).setEntity(null); } } private List<ControlListener> controlListeners = new ArrayList<>(); /** * @param listener the listener to add */ public void addControlListener(ControlListener listener) { checkValid(); controlListeners.add(listener); } /** * @param listener the listener to remove */ public void removeControlListener(ControlListener listener) { checkValid(); controlListeners.remove(listener); } private void notifyControlAdded(Control control) { for (int i = 0; i < controlListeners.size(); i++) { controlListeners.get(i).onControlAdded(control); } } private void notifyControlRemoved(Control control) { for (int i = 0; i < controlListeners.size(); i++) { controlListeners.get(i).onControlRemoved(control); } } /* CONTROL END */ /* COMPONENT BEGIN */ /** * @param type component type * @return true iff entity has a component of given type */ public final boolean hasComponent(Class<? extends Component> type) { checkValid(); return components.containsKey(type); } /** * Returns component of given type, or {@link Optional#empty()} * if type not registered. * * @param type component type * @return component */ public final <T extends Component> Optional<T> getComponent(Class<T> type) { checkValid(); return Optional.ofNullable(getComponentUnsafe(type)); } /** * Returns component of given type, or null if type not registered. * * @param type component type * @return component */ public final <T extends Component> T getComponentUnsafe(Class<T> type) { checkValid(); return type.cast(components.get(type)); } /** * Warning: object allocation. * Cannot be called during update. * * @return array of components */ public final Array<Component> getComponents() { checkValid(); return components.values().toArray(); } /** * Adds given component to this entity. * Only 1 component with the same type can be registered. * Anonymous components are NOT allowed. * * @param component the component * @throws IllegalArgumentException if a component with same type already registered or anonymous * @throws IllegalStateException if components required by the given component are missing */ public final void addComponent(Component component) { checkValid(); Class<? extends Component> type = component.getClass(); if (type.getCanonicalName() == null) { throw new IllegalArgumentException("Anonymous components are not allowed! - " + type.getName()); } if (hasComponent(type)) { throw new IllegalArgumentException("Entity already has a component with type: " + type.getCanonicalName()); } if (component instanceof AbstractComponent) { AbstractComponent c = (AbstractComponent) component; c.setEntity(this); } checkRequirementsMet(component.getClass()); components.put(component.getClass(), component); component.onAdded(this); if (isActive()) world.onComponentAdded(component, this); notifyComponentAdded(component); } /** * Remove a component with given type from this entity. * * @param type type of the component to remove * @throws IllegalArgumentException if the component is required by other components / controls */ public final void removeComponent(Class<? extends Component> type) { checkValid(); Component component = getComponentUnsafe(type); if (component == null) { log.warning("Attempted to remove component but entity doesn't have a component with type: "+ type.getSimpleName()); } else { // if not cleaning, then entity is alive, whether active or not // hence we cannot allow removal if component is required by other components / controls if (!cleaning) { checkNotRequiredByAny(type); } components.remove(component.getClass()); if (isActive()) world.onComponentRemoved(component, this); removeComponentImpl(component); } } /** * Removes all components from this entity. */ public final void removeAllComponents() { checkValid(); for (Component component : components.values()) { removeComponentImpl(component); } components.clear(); } private void removeComponentImpl(Component component) { notifyComponentRemoved(component); component.onRemoved(this); if (component instanceof AbstractComponent) { AbstractComponent c = (AbstractComponent) component; c.setEntity(null); } } private List<ComponentListener> componentListeners = new ArrayList<>(); /** * Register a component listener on this entity. * * @param listener the listener */ public void addComponentListener(ComponentListener listener) { componentListeners.add(listener); } /** * Removed a component listener. * * @param listener the listener */ public void removeComponentListener(ComponentListener listener) { componentListeners.remove(listener); } private void notifyComponentAdded(Component component) { for (int i = 0; i < componentListeners.size(); i++) { componentListeners.get(i).onComponentAdded(component); } } private void notifyComponentRemoved(Component component) { for (int i = 0; i < componentListeners.size(); i++) { componentListeners.get(i).onComponentRemoved(component); } } /* COMPONENT END */ /** * Checks if requirements for given type are met. * * @param type the type whose requirements to check * @throws IllegalStateException if the type requirements are not met */ private void checkRequirementsMet(Class<?> type) { Required[] required = type.getAnnotationsByType(Required.class); for (Required r : required) { if (!hasComponent(r.value())) { throw new IllegalStateException("Required component: [" + r.value().getSimpleName() + "] for: " + type.getSimpleName() + " is missing"); } } } /** * Checks if given type is not required by any other type. * * @param type the type to check * @throws IllegalArgumentException if the type is required by any other type */ private void checkNotRequiredByAny(Class<? extends Component> type) { // check for components for (Class<?> t : components.keys()) { for (Required required : t.getAnnotationsByType(Required.class)) { if (required.value().equals(type)) { throw new IllegalArgumentException("Required component: [" + required.value().getSimpleName() + "] by: " + t.getSimpleName()); } } } // check for controls for (Class<?> t : controls.keys()) { for (Required required : t.getAnnotationsByType(Required.class)) { if (required.value().equals(type)) { throw new IllegalArgumentException("Required component: [" + required.value().getSimpleName() + "] by: " + t.getSimpleName()); } } } } private boolean controlsEnabled = true; /** * Setting this to false will disable each control's update until this has * been set back to true. * * @param b controls enabled flag */ public final void setControlsEnabled(boolean b) { controlsEnabled = b; } /** * @return active property of this entity */ public final ReadOnlyBooleanProperty activeProperty() { return active.getReadOnlyProperty(); } /** * Entity is "active" from the moment it is registered in the world * and until it is removed from the world. * * @return true if entity is active, else false */ public final boolean isActive() { return active.get(); } private Runnable onActive = null; /** * Set a callback for when entity is added to world. * The callback will be executed immediately if entity is already in the world. * * @param action callback */ public final void setOnActive(Runnable action) { if (isActive()) { action.run(); return; } onActive = action; } private Runnable onNotActive = null; /** * Set a callback for when entity is removed from world. * The callback will be executed immediately if entity is already removed from the world. * * @param action callback */ public final void setOnNotActive(Runnable action) { if (!isActive()) { action.run(); return; } onNotActive = action; } private EntityWorld world; /** * @return the world that entity is attached to */ public EntityWorld getWorld() { return world; } /** * Initializes this entity. * * @param world the world to which entity is being attached */ void init(EntityWorld world) { this.world = world; if (onActive != null) onActive.run(); active.set(true); } private boolean updating = false; private boolean delayedRemove = false; /** * Update tick for this entity. * * @param tpf time per frame */ void update(double tpf) { updating = true; if (controlsEnabled) { for (Control c : controls.values()) { if (!c.isPaused()) { c.onUpdate(this, tpf); } } } updating = false; if (delayedRemove) removeFromWorld(); } private boolean cleaning = false; /** * Cleans entity. * Removes all controls and components. * After this the entity cannot be used. */ void clean() { cleaning = true; if (onNotActive != null) onNotActive.run(); active.set(false); removeAllControls(); removeAllComponents(); controlListeners.clear(); componentListeners.clear(); properties.clear(); controlsEnabled = true; world = null; onActive = null; onNotActive = null; } private void checkValid() { if (cleaning && world == null) throw new IllegalStateException("Attempted access a cleaned entity!"); } /** * Remove entity from world. */ public final void removeFromWorld() { checkValid(); if (updating) { delayedRemove = true; } else { world.removeEntity(this); } } /** * Creates a new instance, which is a copy of this entity. * For each copyable component, copy() will be invoked on the component and attached to new instance. * For each copyable control, copy() will be invoked on the control and attached to new instance. * Components and controls that cannot be copied, must be added manually if required. * * @return copy of this entity */ public Entity copy() { checkValid(); return EntityCopier.INSTANCE.copy(this); } /** * Save entity state into bundle. * Only serializable components and controls will be written. * * @param bundle the bundle to write to */ public void save(Bundle bundle) { checkValid(); EntitySerializer.INSTANCE.save(this, bundle); } /** * Load entity state from a bundle. * Only serializable components and controls will be read. * If an entity has a serializable type that is not present in the bundle, * a warning will be logged but no exception thrown. * * @param bundle bundle to read from */ public void load(Bundle bundle) { checkValid(); EntitySerializer.INSTANCE.load(this, bundle); } @Override public String toString() { return "Entity(" + String.join("\n", "components=" + components.values(), "controls=" + controls.values()) + ")"; } }