Переглянути джерело

Merge pull request #517 from Calinou/add-save-load-demo

Add a saving/loading demo with various serialization formats
Aaron Franke 4 роки тому
батько
коміт
555e43c896

+ 20 - 0
2d/save_load/README.md

@@ -0,0 +1,20 @@
+# Saving and Loading (Serialization)
+
+This demo showcases how to save a simple game with each serialization
+format supported by Godot:
+
+- ConfigFile
+- JSON
+
+More formats may be added in the future.
+
+For more information, see this documentation article:
+https://docs.godotengine.org/en/latest/tutorials/io/saving_games.html
+
+Language: GDScript
+
+Renderer: GLES 2
+
+## Screenshots
+
+![Screenshot](screenshots/save_load.png)

+ 7 - 0
2d/save_load/default_env.tres

@@ -0,0 +1,7 @@
+[gd_resource type="Environment" load_steps=2 format=2]
+
+[sub_resource type="ProceduralSky" id=1]
+
+[resource]
+background_mode = 2
+background_sky = SubResource( 1 )

+ 29 - 0
2d/save_load/enemy.gd

@@ -0,0 +1,29 @@
+extends KinematicBody2D
+
+const MOVE_SPEED = 75
+const DAMAGE_PER_SECOND = 15
+
+# The node we should be "attacking" every frame.
+# If `null`, nobody is in range to attack.
+var attacking = null
+
+
+func _process(delta):
+	if attacking:
+		attacking.health -= delta * DAMAGE_PER_SECOND
+
+	# warning-ignore:return_value_discarded
+	move_and_slide(Vector2(MOVE_SPEED, 0))
+
+	# The enemy went outside of the window. Move it back to the left.
+	if position.x >= 732:
+		position.x = -32
+
+
+func _on_AttackArea_body_entered(body):
+	if body.name == "Player":
+		attacking = body
+
+
+func _on_AttackArea_body_exited(_body):
+	attacking = null

+ 31 - 0
2d/save_load/enemy.tscn

@@ -0,0 +1,31 @@
+[gd_scene load_steps=5 format=2]
+
+[ext_resource path="res://enemy.gd" type="Script" id=1]
+[ext_resource path="res://icon.png" type="Texture" id=2]
+
+[sub_resource type="RectangleShape2D" id=1]
+extents = Vector2( 32, 32 )
+
+[sub_resource type="RectangleShape2D" id=2]
+extents = Vector2( 38, 38 )
+
+[node name="Enemy" type="KinematicBody2D" groups=[
+"enemy",
+]]
+position = Vector2( 64, 160 )
+script = ExtResource( 1 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+shape = SubResource( 1 )
+
+[node name="Sprite" type="Sprite" parent="."]
+modulate = Color( 2, 0.6, 0.5, 1 )
+texture = ExtResource( 2 )
+
+[node name="AttackArea" type="Area2D" parent="."]
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="AttackArea"]
+shape = SubResource( 2 )
+
+[connection signal="body_entered" from="AttackArea" to="." method="_on_AttackArea_body_entered"]
+[connection signal="body_exited" from="AttackArea" to="." method="_on_AttackArea_body_exited"]

+ 13 - 0
2d/save_load/gui.gd

@@ -0,0 +1,13 @@
+extends VBoxContainer
+
+
+func _ready():
+	var file = File.new()
+	# Don't allow loading files that don't exist yet.
+	$SaveLoad/LoadConfigFile.disabled = not file.file_exists(ProjectSettings.globalize_path("user://save_config_file.ini"))
+	$SaveLoad/LoadJSON.disabled = not file.file_exists(ProjectSettings.globalize_path("user://save_json.json"))
+
+
+func _on_open_user_data_folder_pressed():
+	# warning-ignore:return_value_discarded
+	OS.shell_open(ProjectSettings.globalize_path("user://"))

BIN
2d/save_load/icon.png


+ 34 - 0
2d/save_load/icon.png.import

@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.png"
+dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0

+ 30 - 0
2d/save_load/player.gd

@@ -0,0 +1,30 @@
+extends KinematicBody2D
+
+# The player's movement speed.
+const MOVE_SPEED = 240
+
+var health = 100 setget set_health
+var motion = Vector2()
+
+onready var progress_bar = $Sprite/ProgressBar
+
+
+func _process(delta):
+	# Player movement (controller-friendly).
+	var velocity = Vector2.ZERO
+	velocity.x = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
+	velocity.y = Input.get_action_strength("move_down") - Input.get_action_strength("move_up")
+	position += velocity * MOVE_SPEED * delta
+
+	# Prevent the player from going outside the window.
+	position.x = clamp(position.x, 32, 700)
+	position.y = clamp(position.y, 32, 536)
+
+func set_health(p_health):
+	health = p_health
+	progress_bar.value = health
+
+	if health <= 0:
+		# The player died.
+		# warning-ignore:return_value_discarded
+		get_tree().reload_current_scene()

+ 60 - 0
2d/save_load/project.godot

@@ -0,0 +1,60 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+;   [section] ; section goes between []
+;   param=value ; assign values to parameters
+
+config_version=4
+
+[application]
+
+config/name="Saving and Loading (Serialization)"
+run/main_scene="res://save_load.tscn"
+config/icon="res://icon.png"
+
+[display]
+
+window/size/height=576
+window/stretch/mode="2d"
+window/stretch/aspect="expand"
+
+[input]
+
+move_up={
+"deadzone": 0.2,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777232,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":-1.0,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null)
+ ]
+}
+move_down={
+"deadzone": 0.2,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777234,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":1.0,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null)
+ ]
+}
+move_left={
+"deadzone": 0.2,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777231,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":-1.0,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":14,"pressure":0.0,"pressed":false,"script":null)
+ ]
+}
+move_right={
+"deadzone": 0.2,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777233,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":1.0,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":15,"pressure":0.0,"pressed":false,"script":null)
+ ]
+}
+
+[rendering]
+
+quality/driver/driver_name="GLES2"
+vram_compression/import_etc=true
+vram_compression/import_etc2=false
+environment/default_clear_color=Color( 0.133333, 0.2, 0.266667, 1 )
+environment/default_environment="res://default_env.tres"

+ 161 - 0
2d/save_load/save_load.tscn

@@ -0,0 +1,161 @@
+[gd_scene load_steps=10 format=2]
+
+[ext_resource path="res://enemy.tscn" type="PackedScene" id=1]
+[ext_resource path="res://gui.gd" type="Script" id=2]
+[ext_resource path="res://save_load_json.gd" type="Script" id=3]
+[ext_resource path="res://save_load_config_file.gd" type="Script" id=4]
+[ext_resource path="res://icon.png" type="Texture" id=5]
+[ext_resource path="res://player.gd" type="Script" id=6]
+
+[sub_resource type="RectangleShape2D" id=1]
+extents = Vector2( 32, 32 )
+
+[sub_resource type="StyleBoxFlat" id=2]
+bg_color = Color( 0.45098, 1, 0.152941, 1 )
+corner_radius_top_left = 16
+corner_radius_top_right = 16
+corner_radius_bottom_right = 16
+corner_radius_bottom_left = 16
+
+[sub_resource type="StyleBoxFlat" id=3]
+bg_color = Color( 0, 0, 0, 0.25098 )
+corner_radius_top_left = 16
+corner_radius_top_right = 16
+corner_radius_bottom_right = 16
+corner_radius_bottom_left = 16
+
+[node name="Node" type="Node"]
+
+[node name="Game" type="Node2D" parent="."]
+position = Vector2( 296, 8 )
+
+[node name="Player" type="KinematicBody2D" parent="Game"]
+position = Vector2( 48, 40 )
+script = ExtResource( 6 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Game/Player"]
+shape = SubResource( 1 )
+
+[node name="Sprite" type="Sprite" parent="Game/Player"]
+texture = ExtResource( 5 )
+
+[node name="ProgressBar" type="ProgressBar" parent="Game/Player/Sprite"]
+margin_left = -32.0
+margin_top = -40.0
+margin_right = 32.0
+margin_bottom = -34.0
+custom_styles/fg = SubResource( 2 )
+custom_styles/bg = SubResource( 3 )
+value = 100.0
+percent_visible = false
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Enemy" parent="Game" instance=ExtResource( 1 )]
+
+[node name="Enemy2" parent="Game" instance=ExtResource( 1 )]
+position = Vector2( 376, 304 )
+
+[node name="Enemy3" parent="Game" instance=ExtResource( 1 )]
+position = Vector2( 232, 464 )
+
+[node name="Control" type="Control" parent="."]
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_left = 8.0
+margin_top = 8.0
+margin_right = -11.9999
+margin_bottom = -12.0
+__meta__ = {
+"_edit_lock_": true,
+"_edit_use_anchors_": false
+}
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Control"]
+margin_right = 269.0
+margin_bottom = 330.0
+custom_constants/separation = 30
+script = ExtResource( 2 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="SaveLoad" type="GridContainer" parent="Control/VBoxContainer"]
+margin_right = 269.0
+margin_bottom = 78.0
+custom_constants/vseparation = 8
+custom_constants/hseparation = 8
+columns = 2
+
+[node name="SaveConfigFile" type="Button" parent="Control/VBoxContainer/SaveLoad"]
+margin_right = 130.0
+margin_bottom = 35.0
+rect_min_size = Vector2( 0, 35 )
+size_flags_horizontal = 3
+text = "Save as ConfigFile"
+script = ExtResource( 4 )
+game_node = NodePath("../../../../Game")
+player_node = NodePath("../../../../Game/Player")
+
+[node name="LoadConfigFile" type="Button" parent="Control/VBoxContainer/SaveLoad"]
+margin_left = 138.0
+margin_right = 268.0
+margin_bottom = 35.0
+rect_min_size = Vector2( 0, 35 )
+size_flags_horizontal = 3
+text = "Load as ConfigFile"
+script = ExtResource( 4 )
+game_node = NodePath("../../../../Game")
+player_node = NodePath("../../../../Game/Player")
+
+[node name="SaveJSON" type="Button" parent="Control/VBoxContainer/SaveLoad"]
+margin_top = 43.0
+margin_right = 130.0
+margin_bottom = 78.0
+rect_min_size = Vector2( 0, 35 )
+size_flags_horizontal = 3
+text = "Save as JSON"
+script = ExtResource( 3 )
+game_node = NodePath("../../../../Game")
+player_node = NodePath("../../../../Game/Player")
+
+[node name="LoadJSON" type="Button" parent="Control/VBoxContainer/SaveLoad"]
+margin_left = 138.0
+margin_top = 43.0
+margin_right = 268.0
+margin_bottom = 78.0
+rect_min_size = Vector2( 0, 35 )
+size_flags_horizontal = 3
+text = "Load as JSON"
+script = ExtResource( 3 )
+game_node = NodePath("../../../../Game")
+player_node = NodePath("../../../../Game/Player")
+
+[node name="OpenUserDataFolder" type="Button" parent="Control/VBoxContainer"]
+margin_top = 108.0
+margin_right = 269.0
+margin_bottom = 143.0
+rect_min_size = Vector2( 0, 35 )
+hint_tooltip = "Click this button to check the saved files using the operating system's file manager."
+text = "Open User Data Folder"
+
+[node name="RichTextLabel" type="RichTextLabel" parent="Control/VBoxContainer"]
+margin_top = 173.0
+margin_right = 269.0
+margin_bottom = 453.0
+rect_min_size = Vector2( 0, 280 )
+custom_constants/line_separation = 4
+bbcode_enabled = true
+bbcode_text = "Use the arrow keys or controller to move the player.
+
+Use the save and load buttons to save/load the game with the respective format (each format is its own savegame)."
+text = "Use the arrow keys or controller to move the player.
+
+Use the save and load buttons to save/load the game with the respective format (each format is its own savegame)."
+
+[connection signal="pressed" from="Control/VBoxContainer/SaveLoad/SaveConfigFile" to="Control/VBoxContainer/SaveLoad/SaveConfigFile" method="save_game"]
+[connection signal="pressed" from="Control/VBoxContainer/SaveLoad/LoadConfigFile" to="Control/VBoxContainer/SaveLoad/LoadConfigFile" method="load_game"]
+[connection signal="pressed" from="Control/VBoxContainer/SaveLoad/SaveJSON" to="Control/VBoxContainer/SaveLoad/SaveJSON" method="save_game"]
+[connection signal="pressed" from="Control/VBoxContainer/SaveLoad/LoadJSON" to="Control/VBoxContainer/SaveLoad/LoadJSON" method="load_game"]
+[connection signal="pressed" from="Control/VBoxContainer/OpenUserDataFolder" to="Control/VBoxContainer" method="_on_open_user_data_folder_pressed"]

+ 52 - 0
2d/save_load/save_load_config_file.gd

@@ -0,0 +1,52 @@
+extends Button
+# This script shows how to save data using Godot's custom ConfigFile format.
+# ConfigFile can store any Godot type natively.
+
+# The root game node (so we can get and instance enemies).
+export(NodePath) var game_node
+# The player node (so we can set/get its health and position).
+export(NodePath) var player_node
+
+const SAVE_PATH = "user://save_config_file.ini"
+
+
+func save_game():
+	var config = ConfigFile.new()
+
+	var player = get_node(player_node)
+	config.set_value("player", "position", player.position)
+	config.set_value("player", "health", player.health)
+
+	var enemies = []
+	for enemy in get_tree().get_nodes_in_group("enemy"):
+		enemies.push_back({
+			position = enemy.position,
+		})
+	config.set_value("enemies", "enemies", enemies)
+
+	config.save(SAVE_PATH)
+
+	get_node("../LoadConfigFile").disabled = false
+
+
+# `load()` is reserved.
+func load_game():
+	var config = ConfigFile.new()
+	config.load(SAVE_PATH)
+
+	var player = get_node(player_node)
+	player.position = config.get_value("player", "position")
+	player.health = config.get_value("player", "health")
+
+	# Remove existing enemies and add new ones.
+	for enemy in get_tree().get_nodes_in_group("enemy"):
+		enemy.queue_free()
+
+	var enemies = config.get_value("enemies", "enemies")
+	# Ensure the node structure is the same when loading.
+	var game = get_node(game_node)
+
+	for enemy_config in enemies:
+		var enemy = preload("res://enemy.tscn").instance()
+		enemy.position = enemy_config.position
+		game.add_child(enemy)

+ 62 - 0
2d/save_load/save_load_json.gd

@@ -0,0 +1,62 @@
+extends Button
+# This script shows how to save data using the JSON file format.
+# JSON is a widely used file format, but not all Godot types can be
+# stored natively. For example, integers get converted into doubles,
+# and to store Vector2 and other non-JSON types you need `var2str()`.
+
+# The root game node (so we can get and instance enemies).
+export(NodePath) var game_node
+# The player node (so we can set/get its health and position).
+export(NodePath) var player_node
+
+const SAVE_PATH = "user://save_json.json"
+
+
+func save_game():
+	var file = File.new()
+	file.open(SAVE_PATH, File.WRITE)
+
+	var player = get_node(player_node)
+	# JSON doesn't support complex types such as Vector2.
+	# `var2str()` can be used to convert any Variant to a String.
+	var save_dict = {
+		player = {
+			position = var2str(player.position),
+			health = var2str(player.health),
+		},
+		enemies = []
+	}
+
+	for enemy in get_tree().get_nodes_in_group("enemy"):
+		save_dict.enemies.push_back({
+			position = var2str(enemy.position),
+		})
+
+	file.store_line(to_json(save_dict))
+
+	get_node("../LoadJSON").disabled = false
+
+
+# `load()` is reserved.
+func load_game():
+	var file = File.new()
+	file.open(SAVE_PATH, File.READ)
+	var save_dict = parse_json(file.get_line())
+
+	var player = get_node(player_node)
+	# JSON doesn't support complex types such as Vector2.
+	# `str2var()` can be used to convert a String to the corresponding Variant.
+	player.position = str2var(save_dict.player.position)
+	player.health = str2var(save_dict.player.health)
+
+	# Remove existing enemies and add new ones.
+	for enemy in get_tree().get_nodes_in_group("enemy"):
+		enemy.queue_free()
+
+	# Ensure the node structure is the same when loading.
+	var game = get_node(game_node)
+
+	for enemy_config in save_dict.enemies:
+		var enemy = preload("res://enemy.tscn").instance()
+		enemy.position = str2var(enemy_config.position)
+		game.add_child(enemy)

+ 0 - 0
2d/save_load/screenshots/.gdignore


BIN
2d/save_load/screenshots/save_load.png