用 Swift 编写的 iOS 基于实体的游戏框架。
该框架使用传统的实体-组件-系统 (Entity-Component-System),以及空间的概念,详情请参考这篇文章
该 pod 还包含一个子 pod GPEngine/Serialization
,允许从 JSON 序列化和反序列化空间/子空间/实体/组件(请参阅 序列化)。
此库的 Swift Package 也可用。
此引擎的工作方式与经典的 ECS 引擎非常相似,但增加了 Spaces
的抽象层。
╔══════════╗
║ Engine ║
╚═════╤════╝ ╔════════╗╔════════╗╔════════╗
├──────╢ Entity ╟╢ Entity ╟╢ Entity ╟...
│ ╚════════╝╚════════╝╚════════╝
│ ╔════════╗╔════════╗╔════════╗
└──────╢ System ╟╢ System ╟╢ System ╟...
╚════════╝╚════════╝╚════════╝
在这种方式下,你无法轻松地隔离实体组,使其在逻辑上分组(例如,将敌人分成前景和背景敌人,以便背景敌人不与玩家互动)。这可以通过组件/类型标志规范来实现,但 Spaces 旨在使其成为一个显式的抽象。
Spaces 是实体的容器,它们独立运作,从而在分组在一起的实体之间提升了一个抽象层。为了帮助实现这种抽象,还提出了 Subspaces
的概念,旨在将相关数据分组,供系统分别作用于空间/实体(例如,物理引擎的单独实例、渲染相机位置等)。
╔══════════╗
║ Engine ║
╚═════╤════╝
╔═══╧═══╗ ╔════════╗╔════════╗╔════════╗
║ Space ╟─┬─╢ Entity ╟╢ Entity ╟╢ Entity ╟...
╚═══╤═══╝ │ ╚════════╝╚════════╝╚════════╝
│ │ ╔══════════╗
│ └─╢ Subspace ║
│ ╚══════════╝
╔═══╧═══╗ ╔════════╗╔════════╗╔════════╗
║ Space ╟─┬─╢ Entity ╟╢ Entity ╟╢ Entity ╟...
╚═══╤═══╝ │ ╚════════╝╚════════╝╚════════╝
│ │ ╔══════════╗╔══════════╗
│ └─╢ Subspace ╟╢ Subspace ║
│ ╚══════════╝╚══════════╝
│
│ ╔════════╗╔════════╗╔════════╗
└──────╢ System ╟╢ System ╟╢ System ╟...
╚════════╝╚════════╝╚════════╝
在这里,实体被分组到 Spaces 中,每个空间彼此完全隔离。 还引入了 Subspaces,因为它们有助于存储将由 Systems 用于处理数据的状态。 关于这一点很酷的一点是,空间只需要添加与它们相关的子空间; 如果一个子空间不打算被渲染(例如,一个不同的游戏房间仍然“活着”但在门后),则不需要向它添加 RenderingSubspace!
系统仍然是全局的,因为理想情况下它们是无状态的(在 Subspaces 的帮助下)。 然后,系统将查询空间以查找具有相关组件的实体以及所需的子空间,如果可用,则使用其声明的逻辑对它们进行操作。 系统始终独立地作用于每个 Space,就好像它们是隔离的经典 ECS 引擎一样。
Xcode 10.2 & Swift 5.0 或更高版本。
GPEngine 可以通过 CocoaPods 获得。 要安装它,只需将以下行添加到你的 Podfile 中
pod "GPEngine"
GPEngine 也可以在 Swift 包中使用
GPEngine 也可以作为 Swift Package 获得
import PackageDescription
let package = Package(
name: "project_name",
dependencies: [
.package(url: "https://github.com/LuizZak/GPEngine.git", from: "4.0.0")
],
targets: []
)
GPEngine 在 MIT 许可证下可用。 有关更多信息,请参见 LICENSE 文件。
(此可选功能可在 pod 'GPEngine/Serialization'
下获得。)
你可以使用 GameSerializer 类来序列化实体或整个空间(以及子空间/实体/组件)。
这使你可以保存部分或完整的游戏状态,以及从预制的 JSON 结构执行游戏状态的数据驱动初始化。
(有关使内容可序列化所需的内容的信息,请参见下面的 序列化要求)。
let myProvider = MyTypeProvider() // Implements SerializationTypeProvider
let gameSerializer = GameSerializer(typeProvider: myProvider)
let mySerialized = try gameSerializer.serialize(myEntity)
// .serialize(_:) returns a Serialized object, call .serialize() on it to receive a JSON that you can store:
let json = mySerialized.serialized()
// Store json somewhere...
要将实体反序列化回来,请使用以下过程
let json = // Retrieve entity JSON from somewhere...
// Retrieve it back to a Serialized object
// If this fails, that means you got a bad JSON :(
let mySerialized = try Serialized.deserialized(from: json)
// Requires explicit ': Entity' type annotation
let myEntity: Entity = try gameSerializer.extract(from: mySerialized)
// If we reached here, entity was deserialized correctly! Success!
序列化/反序列化 Space
的过程类似,并使用相同的方法名称。
从 Serialized.serialized()
返回的序列化 JSON 遵循给定的结构
{
"contentType": "<String - one of the raw values of Serialized.ContentType>",
"typeName": "<String - type name returned by your SerializationTypeProvider to retrieve the class to instantiate back>",
"data": "<Any - this is the JSON returned by the object's serialize() method>"
}
通过将序列化的容器添加到 data
字段,可以将它们嵌套在一起,并使用 GameSerializer.extract<T: Serializable>(from: Serialized)
方法检索它们。 但是,你必须实现自定义逻辑来执行此类操作。
这是一个必须实现的协议,用于提供你自定义的组件/子空间类型,以便在反序列化期间实例化。
GameSerializer 使用序列化的类型名称调用你的类型提供程序,你必须返回一个 Swift 元类型(例如 MyComponent.self
)。
默认情况下,该协议实现了一种方法,用于获取类型的序列化名称,并返回 String(describing: Type.self)
。
可以使用数组来实现一个简单的类型提供程序,该数组用于存储游戏中每个已知的可序列化类型,使用预先实现的 BasicSerializationTypeProvider
协议(前提是每个可序列化的类型最终都采用与其类型匹配的唯一名称)
class Provider: BasicSerializationTypeProvider {
// Requirement from `BasicSerializationTypeProvider`
var serializableTypes: [(Serializable.Type, (JSON, JsonPath) throws -> Serializable))] = [
(MySerializableComponent.self, MySerializableComponent.init),
(MySerializableSubspace.self, MySerializableSubspace.init),
]
// Now `serializedName(for:)`/`deserialized(from:)` are automatically stubbed using `serializableTypes` array.
}
要序列化实体和空间,你需要遵循以下要求
Component
都必须实现 Serializable
协议。Serializable
协议。Serializable
是使用 JSON 编码对象的基本协议
/// Describes an object that can be serialized to and back from a JSON object.
/// Implementers of this protocol should take care of guaranteeing that the inner
/// state of the object remains the same when deserializing from a previously
/// serialized object.
public protocol Serializable {
/// Serializes the state of this component into a JSON object.
///
/// - returns: The serialized state for this object.
func serialized() -> JSON
}
对于解码,BasicSerializationTypeProvider
希望 self.serializableTypes
是一个包含该类型的数组,以及该类型的初始化器函数的引用,该函数将使用以下签名调用
/// Initializes an instance of this type from a given serialized state.
///
/// - parameter json: A state that was previously serialized by an instance
/// of this type using `serialized()`
/// - parameter path: The full JSON path to the serialized object. Used for
/// diagnostics purposes.
/// - throws: Any type of error during deserialization.
(_ json: JSON, _ path: JsonPath) throws -> Serializable
可以将 path
变量提供给各种 JSON
解码方法,以便在发现反序列化错误时提供上下文
struct MyComponent {
let field: Int
init(json: JSON, path: JsonPath) throws {
field = try json[path: "field"].integer(prefixPath: path)
}
}
如果反序列化失败,则会引发带有相应完整 JSON 路径的错误
try MyComponent(
json: ["field": true],
path: aPrefixPath
)
// Throws error: "Expected a value of type 'bool' but found a value of type 'int' @ <root>.aPrefixPath.field"
要检查你的实体和空间是否完全可序列化,请在你的实体或空间上使用 GameSerializer.canSerialize(_:)
& GameSerializer.diagnoseSerialize(on:)
方法。
系统的目标是无状态,因此默认情况下不支持序列化它们。 然而,这不会阻止你将其添加到你的序列化类型提供程序并实现 Serializable
协议,从而使其可序列化。