All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.almasb.fxgl.entity.GameWorld.kt Maven / Gradle / Ivy

/*
 * FXGL - JavaFX Game Library. The MIT License (MIT).
 * Copyright (c) AlmasB ([email protected]).
 * See LICENSE for details.
 */

package com.almasb.fxgl.entity

import com.almasb.fxgl.core.collection.Array
import com.almasb.fxgl.core.collection.PropertyMap
import com.almasb.fxgl.core.collection.UnorderedArray
import com.almasb.fxgl.core.math.FXGLMath
import com.almasb.fxgl.core.reflect.ReflectionUtils
import com.almasb.fxgl.core.util.tryCatchRoot
import com.almasb.fxgl.entity.component.Component
import com.almasb.fxgl.entity.components.IDComponent
import com.almasb.fxgl.entity.components.IrremovableComponent
import com.almasb.fxgl.entity.components.TimeComponent
import com.almasb.fxgl.entity.level.Level
import com.almasb.fxgl.logging.Logger
import javafx.geometry.Point2D
import javafx.geometry.Rectangle2D
import java.util.*
import java.util.function.Function
import java.util.function.Predicate
import kotlin.NoSuchElementException
import kotlin.collections.ArrayList

/**
 * Represents pure logical state of the game.
 * Manages all entities and allows queries.
 *
 * @author Almas Baimagambetov (AlmasB) ([email protected])
 */
class GameWorld {

    companion object {
        private val log = Logger.get("GameWorld")
    }

    val properties = PropertyMap()

    private val updateList = Array()

    /**
     * List of entities added to the update list in the next tick.
     */
    private val waitingList = UnorderedArray()

    /**
     * List of entities in the world.
     *
     * @return direct list of entities in the world (do NOT modify)
     */
    val entities = ArrayList()

    /**
     * @return shallow copy of the entities list (new list)
     */
    val entitiesCopy: List
        get(): List = ArrayList(entities)

    init {
        log.debug("Game world initialized")
    }

    /**
     * The entity will be added to update list in the next tick.
     *
     * @param entity the entity to add to world
     */
    fun addEntity(entity: Entity) {
        require(!entity.isActive) { "Entity is already attached to world" }

        if (entity.isEverUpdated)
            waitingList.add(entity)

        entities.add(entity)

        add(entity)
    }

    fun addEntities(vararg entitiesToAdd: Entity) {
        for (e in entitiesToAdd) {
            addEntity(e)
        }
    }

    private fun add(entity: Entity) {
        entity.init(this)
        notifyEntityAdded(entity)
    }

    /**
     * Entity will be removed from world and parties notified of removal in the same frame.
     * The components and controls of entity will be removed in the next frame to avoid
     * concurrency issues.
     */
    fun removeEntity(entity: Entity) {
        if (!entity.isActive) {
            log.warning("Attempted to remove entity which is not active")
            return
        }

        if (!canRemove(entity))
            return

        require(entity.world === this) { "Attempted to remove entity not attached to this world" }

        entities.remove(entity)

        entity.markForRemoval()
        notifyEntityRemoved(entity)

        // we cannot clean entities here because this may have been called through a component
        // while entity is being updated
        // so, we clean the entity on next frame

        // however, we can clean entities that are never updated
        if (!entity.isEverUpdated)
            entity.clean()
    }

    fun removeEntities(vararg entitiesToRemove: Entity) {
        for (e in entitiesToRemove) {
            removeEntity(e)
        }
    }

    fun removeEntities(entitiesToRemove: Collection) {
        for (e in entitiesToRemove) {
            removeEntity(e)
        }
    }

    private fun canRemove(entity: Entity): Boolean {
        return !entity.hasComponent(IrremovableComponent::class.java)
    }

    /**
     * Performs a single world update tick.
     *
     * @param tpf time per frame
     */
    fun onUpdate(tpf: Double) {
        updateList.addAll(waitingList)
        waitingList.clear()

        val it = updateList.iterator()
        while (it.hasNext()) {
            val e = it.next()

            if (!e.isActive) {
                // clean entities removed in the last frame
                e.clean()
                it.remove()
            } else {
                val tpfRatio = e.getComponentOptional(TimeComponent::class.java)
                        .map { it.value }
                        .orElse(1.0)

                e.update(tpf * tpfRatio)
            }
        }
    }

    /**
     * Resets this game world to its original state (as if newly constructed) by
     * removing all (including with IrremovableComponent) entities, properties, entity factories and world listeners.
     * Do NOT call this method manually.
     * It is called automatically by FXGL during initGame().
     */
    fun reset() {
        log.debug("Clearing game world")

        waitingList.clear()

        // we may still have some entities that have been removed but not yet cleaned
        run {
            val it = updateList.iterator()
            while (it.hasNext()) {
                val e = it.next()

                if (!e.isActive) {
                    e.clean()
                }

                it.remove()
            }
        }

        // entities list does not contain "not active" entities, so we do full clean
        // also we copy since during removal notification components may remove other entities
        entitiesCopy.forEach { e ->
            e.markForRemoval()
            notifyEntityRemoved(e)
            e.clean()
        }

        properties.clear()
        entities.clear()
        entityFactories.clear()
        entitySpawners.clear()
        worldListeners.clear()
    }

    private val worldListeners = Array()

    fun addWorldListener(listener: EntityWorldListener) {
        worldListeners.add(listener)
    }

    fun removeWorldListener(listener: EntityWorldListener) {
        worldListeners.removeValueByIdentity(listener)
    }

    private fun notifyEntityAdded(e: Entity) {
        worldListeners.forEach { it.onEntityAdded(e) }
    }

    private fun notifyEntityRemoved(e: Entity) {
        worldListeners.forEach { it.onEntityRemoved(e) }
    }

    /**
     * Set level to given.
     * Resets the world.
     * Adds all level entities to the world.
     *
     * @param level the level
     */
    fun setLevel(level: Level) {
        clearLevel()

        log.debug("Setting level: $level")
        level.entities.forEach { addEntity(it) }
    }

    /**
     * Removes removable entities.
     */
    private fun clearLevel() {
        log.debug("Clearing removable entities")

        waitingList.removeAll { canRemove(it) }

        // we may still have some entities that have been removed but not yet cleaned
        // but we do not want to remove Irremovables
        run {
            val it = updateList.iterator()
            while (it.hasNext()) {
                val e = it.next()

                if (canRemove(e)) {
                    if (!e.isActive) {
                        // clean it here because "entities" list does not have "e"
                        e.clean()
                    }

                    it.remove()
                }
            }
        }

        // entities list does not contain "not active" entities, so we do full clean
        // but we do not want to remove Irremovables
        val it = entities.iterator()
        while (it.hasNext()) {
            val e = it.next()

            if (canRemove(e)) {
                e.markForRemoval()
                notifyEntityRemoved(e)
                e.clean()

                it.remove()
            }
        }
    }

    private val entityFactories = hashMapOf>()

    /**
     * Maps entity spawn name to the method that creates the entity.
     */
    private val entitySpawners = hashMapOf>()

    /**
     * @param entityFactory factory for creating entities
     */
    fun addEntityFactory(entityFactory: EntityFactory) {
        val entityNames = arrayListOf()

        ReflectionUtils.findMethodsMapToFunctions(entityFactory, Spawns::class.java)
                .forEach { annotation, entitySpawner ->
                    
                    val entityAliases = annotation.value.split(",".toRegex())
                    entityAliases.forEach { entityName ->
                        checkDuplicateSpawners(entityFactory, entityName)

                        entitySpawners.put(entityName, entitySpawner)
                        entityNames.add(entityName)
                    }
                }

//        ReflectionUtils.findMethods(entityFactory, Preload::class.java)
//                .forEach { preload, method ->
//
//                    val ann = method.getDeclaredAnnotation(Spawns::class.java)
//                    val entityAliases = ann.value.split(",".toRegex())
//
//                    val numToPreload = preload.value
//
//                    entityPreloader.addForPreloading(entityAliases, numToPreload)
//                }

        entityFactories.put(entityFactory, entityNames)
    }

    private fun checkDuplicateSpawners(entityFactory: EntityFactory, entityName: String) {
        if (entitySpawners.containsKey(entityName)) {

            // find the factory that already has entityName spawner
            val factory = entityFactories.entries.find { entityName in it.value }

            throw IllegalArgumentException("Duplicated @Spawns($entityName) in $entityFactory. Already exists in $factory")
        }
    }

    fun removeEntityFactory(entityFactory: EntityFactory) {
        entityFactories.remove(entityFactory)?.forEach {
            entitySpawners.remove(it)
        }
    }

    /**
     * Creates an entity with given name at 0, 0 using a previously added entity factory.
     * Adds created entity to this game world.
     *
     * @param entityName name of entity as specified by [Spawns]
     * @return spawned entity
     */
    fun spawn(entityName: String): Entity {
        return spawn(entityName, 0.0, 0.0)
    }

    /**
     * Creates an entity with given name at x, y using a previously added entity factory.
     * Adds created entity to this game world.
     *
     * @param entityName name of entity as specified by [Spawns]
     * @param position spawn location
     * @return spawned entity
     */
    fun spawn(entityName: String, position: Point2D): Entity {
        return spawn(entityName, position.x, position.y)
    }

    /**
     * Creates an entity with given name at x, y using a previously added entity factory.
     * Adds created entity to this game world.
     *
     * @param entityName name of entity as specified by [Spawns]
     * @param x x position
     * @param y y position
     * @return spawned entity
     */
    fun spawn(entityName: String, x: Double, y: Double): Entity {
        return spawn(entityName, SpawnData(x, y))
    }

    /**
     * Creates an entity with given name and data using a previously added entity factory.
     * Adds created entity to this game world.
     *
     * @param entityName name of entity as specified by [Spawns]
     * @param data spawn data, such as x, y and any extra info
     * @return spawned entity
     */
    fun spawn(entityName: String, data: SpawnData): Entity {
        val entity = create(entityName, data)
        addEntity(entity)
        return entity
    }

    /**
     * Creates an entity with given name and data using a previously added entity factory.
     * Does NOT add created entity to the game world.
     *
     * @param entityName name of entity as specified by [Spawns]
     * @param data spawn data, such as x, y and any extra info
     * @return created entity
     */
    fun create(entityName: String, data: SpawnData): Entity {
        check(entityFactories.isNotEmpty()) { "No EntityFactory was added! Call gameWorld.addEntityFactory()" }

        val spawner = entitySpawners.get(entityName)
                ?: throw IllegalArgumentException("No EntityFactory has a method annotated @Spawns($entityName)")

        if (!data.hasKey("type")) {
            data.put("type", entityName)
        }

//        if (entityPreloader.isPreloadingEnabled(entityName)) {
//            return entityPreloader.obtain(entityName, data)
//        }

        return tryCatchRoot { spawner.apply(data) }
    }

    /* QUERIES */

    fun getSingleton(type: Enum<*>): Entity {
        return getSingleton(Predicate { it.isType(type) })
    }

    fun getSingleton(predicate: Predicate): Entity {
        return entities.find { predicate.test(it) } ?: throw NoSuchElementException("No entity found satisfying the predicate")
    }

    /**
     * Useful for singleton type entities, e.g. Player.
     *
     * @return first occurrence matching given type
     */
    fun getSingletonOptional(type: Enum<*>): Optional {
        return getSingletonOptional(Predicate { it.isType(type) })
    }

    /**
     * @return first occurrence matching given predicate
     */
    fun getSingletonOptional(predicate: Predicate): Optional {
        return Optional.ofNullable(entities.find { predicate.test(it) })
    }

    /**
     * @param type entity type
     * @return a random entity with given type
     */
    fun getRandom(type: Enum<*>): Optional {
        return FXGLMath.random(getEntitiesByType(type))
    }

    fun getRandom(predicate: Predicate): Optional {
        return FXGLMath.random(getEntitiesFiltered(predicate))
    }

    /**
     * @param type component type
     * @return array of entities that have given component
     */
    fun getEntitiesByComponent(type: Class): List {
        return entities.filter { it.hasComponent(type) }
    }

    /**
     * Returns a list of entities which are filtered by
     * given predicate.
     *
     * @param predicate filter
     * @return new list containing entities that satisfy query filters
     */
    fun getEntitiesFiltered(predicate: Predicate): List {
        return entities.filter { predicate.test(it) }
    }

    /**
     * If called with no arguments, all entities are returned.
     *
     * @param types entity types
     * @return new list containing entities that satisfy query filters
     */
    fun getEntitiesByType(vararg types: Enum<*>): List {
        if (types.isEmpty())
            return entitiesCopy

        return entities.filter { isOneOfTypes(it, *types) }
    }

    private fun isOneOfTypes(entity: Entity, vararg types: Enum<*>): Boolean {
        return types.any { entity.isType(it) }
    }

    /**
     * Returns a list of entities
     * which are partially or entirely
     * in the specified rectangular selection.
     * Warning: object allocation.
     *
     * @param selection Rectangle2D that describes the selection box
     * @return new list containing entities that satisfy query filters
     */
    fun getEntitiesInRange(selection: Rectangle2D): List {
        return entities.filter { it.boundingBoxComponent.isWithin(selection) }
    }

    /**
     * Returns a list of entities
     * which colliding with given entity.
     *
     * Note: CollidableComponent is not considered.
     *
     * @param entity the entity
     * @return new list containing entities that satisfy query filters
     */
    fun getCollidingEntities(entity: Entity): List {
        return entities.filter { it.isColliding(entity) && it !== entity }
    }

    /**
     * Returns a list of entities at given position.
     * The position x and y must equal to entity's position x and y.
     *
     * @param position point in the world
     * @return entities at given point
     */
    fun getEntitiesAt(position: Point2D): List {
        return entities.filter { it.position == position }
    }

    /**
     * Returns the closest entity to the given entity with given
     * filter. The given
     * entity itself is never returned.
     *
     * If there no entities satisfying the requirement, [Optional.empty]
     * is returned.
     *
     * @param entity selected entity
     * @param filter requirements
     * @return closest entity to selected entity with type
     */
    fun getClosestEntity(entity: Entity, filter: Predicate): Optional {
        return Optional.ofNullable(
                entities.filter { filter.test(it) && it !== entity }
                        .sortedBy { entity.distance(it) }
                        .firstOrNull()
        )
    }

    /**
     * Returns an entity whose IDComponent matches given name and id.
     *
     *
     * Returns [Optional.empty] if no entity was found with such combination.
     * This query only works on entities with IDComponent.
     *
     * @param name entity name
     * @param id entity id
     * @return entity that matches the query or [Optional.empty]
     */
    fun getEntityByID(name: String, id: Int): Optional {
        return Optional.ofNullable(
                getEntitiesByComponent(IDComponent::class.java).find {
                    val idComponent = it.getComponent(IDComponent::class.java)

                    return@find idComponent.name == name && idComponent.id == id
                }
        )
    }

    /**
     * @return entity group of given types
     */
    fun getGroup(vararg entityTypes: Enum<*>) = EntityGroup(this, getEntitiesByType(*entityTypes), *entityTypes)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy