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_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"]
|
||||
advance_mode = 2
|
||||
advance_expression = "is_firing()"
|
||||
|
@ -70,18 +74,14 @@ advance_expression = "is_firing()"
|
|||
switch_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"]
|
||||
states/Aim/node = SubResource("AnimationNodeAnimation_d2wvv")
|
||||
states/Aim/position = Vector2(551, 100)
|
||||
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/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"]
|
||||
animation = &"Idle"
|
||||
|
@ -162,7 +162,7 @@ tree_root = SubResource("AnimationNodeStateMachine_5lvsk")
|
|||
advance_expression_base_node = NodePath("..")
|
||||
anim_player = NodePath("../Mesh/AnimationPlayer")
|
||||
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
|
||||
script = ExtResource("3_26yay")
|
||||
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 {
|
||||
private static _instance: AsyncResourceLoader
|
||||
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 {
|
||||
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) => {
|
||||
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) {
|
||||
return reject(new Error(`failed to load ${path}`))
|
||||
return reject(new GodotError(err))
|
||||
}
|
||||
|
||||
const get_status = () => ResourceLoader.load_threaded_get_status(path)
|
||||
|
@ -27,12 +44,18 @@ export default class AsyncResourceLoader extends Node {
|
|||
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)
|
||||
} 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_node, onready } from './annotations'
|
||||
import Weapon from './weapon'
|
||||
import AsyncResourceLoader from './async_resource_loader'
|
||||
import AsyncResourceHandler from './async_resource_handler'
|
||||
import { forward } from './vec'
|
||||
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'
|
||||
|
||||
const { MULTIPLY: mul, ADD: add } = Vector3
|
||||
|
@ -36,7 +36,7 @@ export default class EquippedWeapon extends Node3D {
|
|||
|
||||
_ready(): void {
|
||||
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) {
|
||||
AsyncResourceLoader.instance.load<PackedScene>(weapon.scene, 'PackedScene')
|
||||
AsyncResourceHandler.instance.load<PackedScene>(weapon.scene, 'PackedScene')
|
||||
.then(scene => this._parent_scene_to_transform(scene))
|
||||
|
||||
if (this._has_equipped_weapon) {
|
||||
|
@ -86,7 +86,7 @@ export default class EquippedWeapon extends Node3D {
|
|||
if (!result.is_empty()) {
|
||||
DebugDraw.draw_ray(query, Color.DARK_RED)
|
||||
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) {
|
||||
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 {
|
||||
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 {
|
||||
|
|
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