DemoBots/Rules/LevelStateSnapshot.swift
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
These types are used by the game's AI to capture and evaluate a snapshot of the game's state. `EntityDistance` encapsulates the distance between two entities. `LevelStateSnapshot` stores an `EntitySnapshot` for every entity in the level. `EntitySnapshot` stores the distances from an entity to every other entity in the level. |
*/ |
import GameplayKit |
/// Encapsulates two entities and their distance apart. |
struct EntityDistance { |
let source: GKEntity |
let target: GKEntity |
let distance: Float |
} |
/** |
Stores a snapshot of the state of a level and all of its entities |
(`PlayerBot`s and `TaskBot`s) at a certain point in time. |
*/ |
class LevelStateSnapshot { |
// MARK: Properties |
/// A dictionary whose keys are entities, and whose values are entity snapshots for those entities. |
var entitySnapshots: [GKEntity: EntitySnapshot] = [:] |
// MARK: Initialization |
/// Initializes a new `LevelStateSnapshot` representing all of the entities in a `LevelScene`. |
init(scene: LevelScene) { |
/// Returns the `GKAgent2D` for a `PlayerBot` or `TaskBot`. |
func agentForEntity(entity: GKEntity) -> GKAgent2D { |
if let agent = entity.component(ofType: TaskBotAgent.self) { |
return agent |
} |
else if let playerBot = entity as? PlayerBot { |
return playerBot.agent |
} |
fatalError("All entities in a level must have an accessible associated GKEntity") |
} |
// A dictionary that will contain a temporary array of `EntityDistance` instances for each entity. |
var entityDistances: [GKEntity: [EntityDistance]] = [:] |
// Add an empty array to the dictionary for each entity, ready for population below. |
for entity in scene.entities { |
entityDistances[entity] = [] |
} |
/* |
Iterate over all entities in the scene to calculate their distance from other entities. |
`scene.entities` is a `Set`, which does not have integer indexing. |
Because we want to use the current index value from the outer loop as the seed for the inner loop, |
we work with the `Set` index values directly. |
*/ |
for sourceEntity in scene.entities { |
let sourceIndex = scene.entities.index(of: sourceEntity)! |
// Retrieve the `GKAgent` for the source entity. |
let sourceAgent = agentForEntity(entity: sourceEntity) |
// Iterate over the remaining entities to calculate their distance from the source agent. |
for targetEntity in scene.entities[scene.entities.index(after: sourceIndex) ..< scene.entities.endIndex] { |
// Retrieve the `GKAgent` for the target entity. |
let targetAgent = agentForEntity(entity: targetEntity) |
// Calculate the distance between the two agents. |
let dx = targetAgent.position.x - sourceAgent.position.x |
let dy = targetAgent.position.y - sourceAgent.position.y |
let distance = hypotf(dx, dy) |
// Save this distance to both the source and target entity distance arrays. |
entityDistances[sourceEntity]!.append(EntityDistance(source: sourceEntity, target: targetEntity, distance: distance)) |
entityDistances[targetEntity]!.append(EntityDistance(source: targetEntity, target: sourceEntity, distance: distance)) |
} |
} |
// Determine the number of "good" `TaskBot`s and "bad" `TaskBot`s in the scene. |
let (goodTaskBots, badTaskBots) = scene.entities.reduce(([], [])) { |
(workingArrays: (goodBots: [TaskBot], badBots: [TaskBot]), thisEntity: GKEntity) -> ([TaskBot], [TaskBot]) in |
// Try to cast this entity as a `TaskBot`, and skip this entity if the cast fails. |
guard let thisTaskBot = thisEntity as? TaskBot else { return workingArrays } |
// Add this `TaskBot` to the appropriate working array based on whether it is "good" or not. |
if thisTaskBot.isGood { |
return (workingArrays.goodBots + [thisTaskBot], workingArrays.badBots) |
} |
else { |
return (workingArrays.goodBots, workingArrays.badBots + [thisTaskBot]) |
} |
} |
let badBotPercentage = Float(badTaskBots.count) / Float(goodTaskBots.count + badTaskBots.count) |
// Create and store an entity snapshot in the `entitySnapshots` dictionary for each entity. |
for entity in scene.entities { |
let entitySnapshot = EntitySnapshot(badBotPercentage: badBotPercentage, proximityFactor: scene.levelConfiguration.proximityFactor, entityDistances: entityDistances[entity]!) |
entitySnapshots[entity] = entitySnapshot |
} |
} |
} |
class EntitySnapshot { |
// MARK: Properties |
/// Percentage of `TaskBot`s in the level that are bad. |
let badBotPercentage: Float |
/// The factor used to normalize distances between characters for 'fuzzy' logic. |
let proximityFactor: Float |
/// Distance to the `PlayerBot` if it is targetable. |
let playerBotTarget: (target: PlayerBot, distance: Float)? |
/// The nearest "good" `TaskBot`. |
let nearestGoodTaskBotTarget: (target: TaskBot, distance: Float)? |
/// A sorted array of distances from this entity to every other entity in the level. |
let entityDistances: [EntityDistance] |
// MARK: Initialization |
init(badBotPercentage: Float, proximityFactor: Float, entityDistances: [EntityDistance]) { |
self.badBotPercentage = badBotPercentage |
self.proximityFactor = proximityFactor |
// Sort the `entityDistances` array by distance (nearest first), and store the sorted version. |
self.entityDistances = entityDistances.sorted { |
return $0.distance < $1.distance |
} |
var playerBotTarget: (target: PlayerBot, distance: Float)? |
var nearestGoodTaskBotTarget: (target: TaskBot, distance: Float)? |
/* |
Iterate over the sorted `entityDistances` array to find the `PlayerBot` |
(if it is targetable) and the nearest "good" `TaskBot`. |
*/ |
for entityDistance in self.entityDistances { |
if let target = entityDistance.target as? PlayerBot, playerBotTarget == nil && target.isTargetable { |
playerBotTarget = (target: target, distance: entityDistance.distance) |
} |
else if let target = entityDistance.target as? TaskBot, nearestGoodTaskBotTarget == nil && target.isGood { |
nearestGoodTaskBotTarget = (target: target, distance: entityDistance.distance) |
} |
// Stop iterating over the array once we have found both the `PlayerBot` and the nearest good `TaskBot`. |
if playerBotTarget != nil && nearestGoodTaskBotTarget != nil { |
break |
} |
} |
self.playerBotTarget = playerBotTarget |
self.nearestGoodTaskBotTarget = nearestGoodTaskBotTarget |
} |
} |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-09-13