wip save system
This commit is contained in:
parent
0f1d693be6
commit
ba9a169288
11 changed files with 227 additions and 22 deletions
|
@ -62,6 +62,10 @@ advance_mode = 2
|
||||||
advance_mode = 2
|
advance_mode = 2
|
||||||
advance_expression = "is_aiming()"
|
advance_expression = "is_aiming()"
|
||||||
|
|
||||||
|
[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_qfm1y"]
|
||||||
|
advance_mode = 2
|
||||||
|
advance_expression = "not is_aiming()"
|
||||||
|
|
||||||
[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_oprun"]
|
[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_oprun"]
|
||||||
advance_mode = 2
|
advance_mode = 2
|
||||||
advance_expression = "is_firing()"
|
advance_expression = "is_firing()"
|
||||||
|
@ -70,18 +74,14 @@ advance_expression = "is_firing()"
|
||||||
switch_mode = 2
|
switch_mode = 2
|
||||||
advance_mode = 2
|
advance_mode = 2
|
||||||
|
|
||||||
[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_qfm1y"]
|
|
||||||
advance_mode = 2
|
|
||||||
advance_expression = "not is_aiming()"
|
|
||||||
|
|
||||||
[sub_resource type="AnimationNodeStateMachine" id="AnimationNodeStateMachine_3v2ag"]
|
[sub_resource type="AnimationNodeStateMachine" id="AnimationNodeStateMachine_3v2ag"]
|
||||||
states/Aim/node = SubResource("AnimationNodeAnimation_d2wvv")
|
states/Aim/node = SubResource("AnimationNodeAnimation_d2wvv")
|
||||||
states/Aim/position = Vector2(551, 100)
|
states/Aim/position = Vector2(551, 100)
|
||||||
states/Fire/node = SubResource("AnimationNodeAnimation_3v2ag")
|
states/Fire/node = SubResource("AnimationNodeAnimation_3v2ag")
|
||||||
states/Fire/position = Vector2(687, 100)
|
states/Fire/position = Vector2(730, 100)
|
||||||
states/Movement/node = SubResource("AnimationNodeBlendTree_d2wvv")
|
states/Movement/node = SubResource("AnimationNodeBlendTree_d2wvv")
|
||||||
states/Movement/position = Vector2(403, 100)
|
states/Movement/position = Vector2(403, 100)
|
||||||
transitions = ["Start", "Movement", SubResource("AnimationNodeStateMachineTransition_jej6c"), "Movement", "Aim", SubResource("AnimationNodeStateMachineTransition_f1ej7"), "Aim", "Fire", SubResource("AnimationNodeStateMachineTransition_oprun"), "Fire", "Aim", SubResource("AnimationNodeStateMachineTransition_a8ls1"), "Aim", "Movement", SubResource("AnimationNodeStateMachineTransition_qfm1y")]
|
transitions = ["Start", "Movement", SubResource("AnimationNodeStateMachineTransition_jej6c"), "Movement", "Aim", SubResource("AnimationNodeStateMachineTransition_f1ej7"), "Aim", "Movement", SubResource("AnimationNodeStateMachineTransition_qfm1y"), "Aim", "Fire", SubResource("AnimationNodeStateMachineTransition_oprun"), "Fire", "Aim", SubResource("AnimationNodeStateMachineTransition_a8ls1")]
|
||||||
|
|
||||||
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_sk752"]
|
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_sk752"]
|
||||||
animation = &"Idle"
|
animation = &"Idle"
|
||||||
|
@ -162,7 +162,7 @@ tree_root = SubResource("AnimationNodeStateMachine_5lvsk")
|
||||||
advance_expression_base_node = NodePath("..")
|
advance_expression_base_node = NodePath("..")
|
||||||
anim_player = NodePath("../Mesh/AnimationPlayer")
|
anim_player = NodePath("../Mesh/AnimationPlayer")
|
||||||
parameters/Handgun/Movement/Blend2/blend_amount = 1.0
|
parameters/Handgun/Movement/Blend2/blend_amount = 1.0
|
||||||
parameters/Handgun/Movement/BlendSpace1D/blend_position = 0.229277
|
parameters/Handgun/Movement/BlendSpace1D/blend_position = 0.5
|
||||||
parameters/Unarmed/blend_position = 0.872134
|
parameters/Unarmed/blend_position = 0.872134
|
||||||
script = ExtResource("3_26yay")
|
script = ExtResource("3_26yay")
|
||||||
move_speed_expression = "move_speed()"
|
move_speed_expression = "move_speed()"
|
||||||
|
|
73
src/async_resource_handler.ts
Normal file
73
src/async_resource_handler.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { GArray, GError, Node, Resource, ResourceLoader, ResourceSaver } from 'godot'
|
||||||
|
import { GodotError } from './godot_error'
|
||||||
|
|
||||||
|
export class ResourceLoadError extends Error { }
|
||||||
|
|
||||||
|
export default class AsyncResourceHandler extends Node {
|
||||||
|
private static _instance: AsyncResourceHandler
|
||||||
|
static readonly SaveFlags = ResourceSaver.SaverFlags
|
||||||
|
static readonly CacheMode = ResourceLoader.CacheMode
|
||||||
|
|
||||||
|
static get instance() {
|
||||||
|
return this._instance
|
||||||
|
}
|
||||||
|
|
||||||
|
_ready(): void {
|
||||||
|
AsyncResourceHandler._instance = this
|
||||||
|
}
|
||||||
|
|
||||||
|
static save<T extends Resource>(path: string, data: T, flags: ResourceSaver.SaverFlags): Promise<void> {
|
||||||
|
return this._instance.save(path, data, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
save<T extends Resource>(path: string, data: T, flags: ResourceSaver.SaverFlags): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const err = ResourceSaver.save(data, path, flags)
|
||||||
|
|
||||||
|
if (err === GError.OK) {
|
||||||
|
resolve()
|
||||||
|
} else {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static load<T extends Resource>(path: string, hint_string: string = '', use_sub_threads?: boolean, cache_mode?: ResourceLoader.CacheMode): Promise<T> {
|
||||||
|
return this._instance.load(path, hint_string, use_sub_threads, cache_mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
load<T extends Resource>(path: string, hint_string: string = '', use_sub_threads?: boolean, cache_mode?: ResourceLoader.CacheMode): Promise<T> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const err = ResourceLoader.load_threaded_request(path, hint_string, use_sub_threads, cache_mode)
|
||||||
|
|
||||||
|
if (err != GError.OK) {
|
||||||
|
return reject(new GodotError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
const get_status = () => ResourceLoader.load_threaded_get_status(path)
|
||||||
|
|
||||||
|
let status = get_status()
|
||||||
|
while (status === ResourceLoader.ThreadLoadStatus.THREAD_LOAD_IN_PROGRESS) {
|
||||||
|
await this.get_tree().process_frame.as_promise()
|
||||||
|
status = get_status()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === ResourceLoader.ThreadLoadStatus.THREAD_LOAD_LOADED) {
|
||||||
|
return resolve(ResourceLoader.load_threaded_get(path) as T)
|
||||||
|
} else {
|
||||||
|
return reject(new ResourceLoadError())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static get_progress(path: string): number {
|
||||||
|
return this._instance.get_progress(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
get_progress(path: string): number {
|
||||||
|
const arr = new GArray()
|
||||||
|
ResourceLoader.load_threaded_get_status(path, arr)
|
||||||
|
return arr.get_indexed(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
1
src/async_resource_handler.ts.uid
Normal file
1
src/async_resource_handler.ts.uid
Normal file
|
@ -0,0 +1 @@
|
||||||
|
uid://byri2tpbtahpd
|
|
@ -1,22 +1,39 @@
|
||||||
import { GError, Node, Resource, ResourceLoader } from 'godot'
|
import { GArray, GError, Node, Resource, ResourceLoader, ResourceSaver } from 'godot'
|
||||||
|
import { GodotError } from './godot_error'
|
||||||
|
|
||||||
export default class AsyncResourceLoader extends Node {
|
export class ResourceLoadError extends Error { }
|
||||||
private static _instance: AsyncResourceLoader
|
|
||||||
|
export default class AsyncResourceHandler extends Node {
|
||||||
|
private static _instance: AsyncResourceHandler
|
||||||
|
static readonly SaveFlags = ResourceSaver.SaverFlags
|
||||||
|
static readonly CacheMode = ResourceLoader.CacheMode
|
||||||
|
|
||||||
static get instance() {
|
static get instance() {
|
||||||
return this._instance
|
return this._instance
|
||||||
}
|
}
|
||||||
|
|
||||||
_ready(): void {
|
_ready(): void {
|
||||||
AsyncResourceLoader._instance = this
|
AsyncResourceHandler._instance = this
|
||||||
}
|
}
|
||||||
|
|
||||||
load<T extends Resource>(path: string, hint_string: string = ''): Promise<T> {
|
save<T extends Resource>(path: string, data: T, flags: ResourceSaver.SaverFlags): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const err = ResourceSaver.save(data, path, flags)
|
||||||
|
|
||||||
|
if (err === GError.OK) {
|
||||||
|
resolve()
|
||||||
|
} else {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
load<T extends Resource>(path: string, hint_string: string = '', use_sub_threads?: boolean, cache_mode?: ResourceLoader.CacheMode): Promise<T> {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
const err = ResourceLoader.load_threaded_request(path, hint_string)
|
const err = ResourceLoader.load_threaded_request(path, hint_string, use_sub_threads, cache_mode)
|
||||||
|
|
||||||
if (err != GError.OK) {
|
if (err != GError.OK) {
|
||||||
return reject(new Error(`failed to load ${path}`))
|
return reject(new GodotError(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
const get_status = () => ResourceLoader.load_threaded_get_status(path)
|
const get_status = () => ResourceLoader.load_threaded_get_status(path)
|
||||||
|
@ -27,12 +44,18 @@ export default class AsyncResourceLoader extends Node {
|
||||||
status = get_status()
|
status = get_status()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get_status() === ResourceLoader.ThreadLoadStatus.THREAD_LOAD_LOADED) {
|
if (status === ResourceLoader.ThreadLoadStatus.THREAD_LOAD_LOADED) {
|
||||||
return resolve(ResourceLoader.load_threaded_get(path) as T)
|
return resolve(ResourceLoader.load_threaded_get(path) as T)
|
||||||
} else {
|
} else {
|
||||||
return reject(new Error(`failed to load ${path}`))
|
return reject(new ResourceLoadError())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_progress(path: string): number {
|
||||||
|
const arr = new GArray()
|
||||||
|
ResourceLoader.load_threaded_get_status(path, arr)
|
||||||
|
return arr.get_indexed(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { Color, GArray, Node, Node3D, PackedScene, PhysicsBody3D, PhysicsRayQuer
|
||||||
import { export_file, signal } from 'godot.annotations'
|
import { export_file, signal } from 'godot.annotations'
|
||||||
import { export_node, onready } from './annotations'
|
import { export_node, onready } from './annotations'
|
||||||
import Weapon from './weapon'
|
import Weapon from './weapon'
|
||||||
import AsyncResourceLoader from './async_resource_loader'
|
import AsyncResourceHandler from './async_resource_handler'
|
||||||
import { forward } from './vec'
|
import { forward } from './vec'
|
||||||
import DebugDraw from './debug_draw'
|
import DebugDraw from './debug_draw'
|
||||||
import { find_in_anscestors, find_in_descendents, implements_interface } from './node'
|
import { find_in_anscestors, find_in_descendents, has_method } from './node'
|
||||||
import { Damageable } from './health'
|
import { Damageable } from './health'
|
||||||
|
|
||||||
const { MULTIPLY: mul, ADD: add } = Vector3
|
const { MULTIPLY: mul, ADD: add } = Vector3
|
||||||
|
@ -36,7 +36,7 @@ export default class EquippedWeapon extends Node3D {
|
||||||
|
|
||||||
_ready(): void {
|
_ready(): void {
|
||||||
if (this.starting_weapon != null) {
|
if (this.starting_weapon != null) {
|
||||||
AsyncResourceLoader.instance.load<Weapon>(this.starting_weapon, 'Weapon').then(weapon => this.equip(weapon))
|
AsyncResourceHandler.instance.load<Weapon>(this.starting_weapon, 'Weapon').then(weapon => this.equip(weapon))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ export default class EquippedWeapon extends Node3D {
|
||||||
}
|
}
|
||||||
|
|
||||||
equip(weapon: Weapon) {
|
equip(weapon: Weapon) {
|
||||||
AsyncResourceLoader.instance.load<PackedScene>(weapon.scene, 'PackedScene')
|
AsyncResourceHandler.instance.load<PackedScene>(weapon.scene, 'PackedScene')
|
||||||
.then(scene => this._parent_scene_to_transform(scene))
|
.then(scene => this._parent_scene_to_transform(scene))
|
||||||
|
|
||||||
if (this._has_equipped_weapon) {
|
if (this._has_equipped_weapon) {
|
||||||
|
@ -86,7 +86,7 @@ export default class EquippedWeapon extends Node3D {
|
||||||
if (!result.is_empty()) {
|
if (!result.is_empty()) {
|
||||||
DebugDraw.draw_ray(query, Color.DARK_RED)
|
DebugDraw.draw_ray(query, Color.DARK_RED)
|
||||||
const root = find_in_anscestors(result.get('collider'), n => n instanceof PhysicsBody3D)
|
const root = find_in_anscestors(result.get('collider'), n => n instanceof PhysicsBody3D)
|
||||||
const health = find_in_descendents<Node & Damageable>(root, n => implements_interface<Damageable>(['apply_damage'], n))
|
const health = find_in_descendents<Node & Damageable>(root, n => has_method('apply_damage', n))
|
||||||
if (health != null) {
|
if (health != null) {
|
||||||
health.apply_damage(this, this._equipped_weapon.damage)
|
health.apply_damage(this, this._equipped_weapon.damage)
|
||||||
}
|
}
|
||||||
|
|
12
src/godot_error.ts
Normal file
12
src/godot_error.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { GError } from "godot";
|
||||||
|
|
||||||
|
export class GodotError extends Error {
|
||||||
|
error: GError
|
||||||
|
|
||||||
|
constructor(err: GError, message?: string) {
|
||||||
|
super(message || `Godot error (${err})`)
|
||||||
|
this.error = err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
1
src/godot_error.ts.uid
Normal file
1
src/godot_error.ts.uid
Normal file
|
@ -0,0 +1 @@
|
||||||
|
uid://o3gcj3wh78yt
|
|
@ -11,8 +11,14 @@ export function queue_free_children<T extends Node>(node: T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RequiredField<T, Field extends keyof T> = Omit<T, Field> & Required<Pick<T, Field>>
|
||||||
|
|
||||||
|
export function has_method(name: StringName, obj: Object): boolean {
|
||||||
|
return obj.has_method(name)
|
||||||
|
}
|
||||||
|
|
||||||
export function implements_interface<T>(method_names: StringName[], obj: Object): obj is Object & T {
|
export function implements_interface<T>(method_names: StringName[], obj: Object): obj is Object & T {
|
||||||
return method_names.every(name => obj.has_method(name))
|
return method_names.every(name => has_method(name, obj))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function find_in_anscestors<T extends Node>(root: T | null | undefined, predicate: Predicate<Node>, height: number = Infinity): T | null | undefined {
|
export function find_in_anscestors<T extends Node>(root: T | null | undefined, predicate: Predicate<Node>, height: number = Infinity): T | null | undefined {
|
||||||
|
|
1
src/node.ts.uid
Normal file
1
src/node.ts.uid
Normal file
|
@ -0,0 +1 @@
|
||||||
|
uid://d0npi365fttf6
|
87
src/save_serializer.ts
Normal file
87
src/save_serializer.ts
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import { GDictionary, Node, PackedScene, Resource, ResourceLoader, ResourceSaver } from 'godot'
|
||||||
|
import AsyncResourceHandler from './async_resource_handler'
|
||||||
|
import { has_method } from './node'
|
||||||
|
|
||||||
|
interface Serializable {
|
||||||
|
on_save_game(save_data: GameState): void
|
||||||
|
on_before_load_game?(): void
|
||||||
|
on_load_game<T extends SaveState>(state: T): void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SaveState extends Resource {
|
||||||
|
scene: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayerState extends Resource implements SaveState {
|
||||||
|
condition: undefined = undefined
|
||||||
|
inventory: undefined = undefined
|
||||||
|
scene: string = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProgressState extends Resource {
|
||||||
|
variables?: GDictionary
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GameState extends Resource {
|
||||||
|
player?: PlayerState
|
||||||
|
progress?: ProgressState
|
||||||
|
world?: SaveState[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const StateMethod = {
|
||||||
|
Save: 'on_save_game',
|
||||||
|
BeforeLoad: 'on_before_load_game',
|
||||||
|
Load: 'on_load_game'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
type StateMethod = typeof StateMethod[keyof typeof StateMethod]
|
||||||
|
|
||||||
|
export default class SaveSerializer extends Node {
|
||||||
|
private static _save_group: string = 'save'
|
||||||
|
|
||||||
|
private static _instance: SaveSerializer
|
||||||
|
|
||||||
|
_ready() {
|
||||||
|
SaveSerializer._instance = this
|
||||||
|
}
|
||||||
|
|
||||||
|
static save() {
|
||||||
|
this._instance.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
private _announce(method: StateMethod, ...args: any) {
|
||||||
|
this.get_tree().call_group(
|
||||||
|
SaveSerializer._save_group,
|
||||||
|
method,
|
||||||
|
...args
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
const game_state: GameState = new GameState()
|
||||||
|
|
||||||
|
this._announce(StateMethod.Save, game_state)
|
||||||
|
|
||||||
|
ResourceSaver.save(game_state, 'user://save1.tres')
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _restore_node<T extends SaveState>(state: T, parent: Node) {
|
||||||
|
const scene: PackedScene = await AsyncResourceHandler.load(state.scene, 'PackedScene')
|
||||||
|
const node = scene.instantiate()
|
||||||
|
parent.add_child(node)
|
||||||
|
|
||||||
|
if (has_method(StateMethod.Load, node)) {
|
||||||
|
// TODO: type check for state method
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
const loaded_state: GameState = await AsyncResourceHandler.load('user://save1.tres')
|
||||||
|
this._announce(StateMethod.BeforeLoad)
|
||||||
|
// TODO: pass world root as 2nd param
|
||||||
|
const node_promises = loaded_state.world?.map(state => this._restore_node(state, this)) ?? []
|
||||||
|
return Promise.all(node_promises)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
1
src/save_serializer.ts.uid
Normal file
1
src/save_serializer.ts.uid
Normal file
|
@ -0,0 +1 @@
|
||||||
|
uid://xv4ix5esb3ij
|
Loading…
Add table
Reference in a new issue