You obviously didn't read my previous comment. It's an epic fail because a JSON file could of been included that defines the expected sprite "grid" layout, enabling developers like I to spend less time editing images, and more time creating content. In fact, I proved this only takes a few minutes to accomplish since I did just that in GoDot. Here is the source code to prove it.
animations.json:
{
"max_column_length": 13,
"max_row_length": 21,
"casting": {
"columns": 7,
"rows": 4,
"up": 0,
"left": 13,
"down": 26,
"right": 39
},
"spear": {
"columns": 8,
"rows": 4,
"up": 52,
"left": 65,
"down": 78,
"right": 91
},
"walking": {
"columns": 9,
"rows": 4,
"up": 104,
"left": 117,
"down": 130,
"right": 143
},
"sword": {
"columns": 6,
"rows": 4,
"up": 156,
"left": 169,
"down": 182,
"right": 195
},
"bow": {
"columns": 13,
"rows": 4,
"up": 208,
"left": 221,
"down": 234,
"right": 247
},
"dead": {
"columns": 6,
"rows": 1,
"down": 260
}
}
This grid layout defines the Universal LPC spritesheets and can easily be modified to work with any spritesheet layout. See the idea?
The other piece of this code is my Sprite.gd script file which loads the animations.json file to determine how to read each individual frame and animation.
Sprite.gd:
extends Sprite
# Sprite animations sub-image offsets and make-up information
var animations = {} # loaded from animations.json
var shop = {} # Only applies to NPC's that are merchants (loaded from npcs.json)
var name = null # unique sprite name
var facing = "down" # direction sprite is facing
var is_moving = false # is our sprite moving?
var world = 1
var merchant = false
var temp_elapsed = 0 # time since last _process() call
var last_index = 0 # last used animation frame index
var animation_speed = 0.15 # miliseconds til next animation frame renders
var current_animation = "walking"
var movement_speed = 1 # movement in pixels
var using_path = false # are we using a PathFollow2D node
var path_movement_speed = 50 # speed when following a PathFollow2D node.
var current_position = Vector2(0, 0) # current sprite position when using PathFollow2D node.
var previous_position = Vector2(0, 0) # previous sprite position when using PathFollow2D node.
var positions = [] # stores last two previous positions when using PathFollow2D node.
# Child nodes
onready var facial_node = get_node("Facial") # Facial (beard, mustache, etc)
onready var hair_node = get_node("Hair")
onready var head_node = get_node("Head")
onready var neck_node = get_node("Neck")
onready var torso_node = get_node("Torso")
onready var belt_node = get_node("Belt")
onready var legs_node = get_node("Legs")
onready var feet_node = get_node("Feet")
onready var weapon_node = get_node("Weapon")
onready var formal_node = get_node("Formal")
onready var hands_node = get_node("Hands")
onready var behind_node = get_node("Behind")
onready var name_node = get_node("Name")
onready var children = self.get_children() # List of all child sprite nodes
func paperdoll(type, action, spritesheet=null):
# type: defined in 'types' dictionary below
# action: 'show', 'hide', 'delete', 'set'
# spritesheet: path to spritesheet
# adds or removes a paperdoll on top of our sprite
var types = {
"head": head_node,
"hair": hair_node,
"facial": facial_node,
"neck": neck_node,
"torso": torso_node,
"belt": belt_node,
"legs": legs_node,
"feet": feet_node,
"weapon": weapon_node,
"formal": formal_node,
"hands": hands_node,
"behind": behind_node,
}
if types.has(type):
var type_node = types[type]
if action == "show":
type_node.show()
elif action == "hide":
type_node.hide()
elif action == "set":
if spritesheet:
var imagetexture = ImageTexture.new()
imagetexture.load(spritesheet)
type_node.set_texture(imagetexture)
elif action == "delete":
type_node.queue_free()
func _fixed_process(delta):
# for pathfollow2d node.
if using_path:
get_parent().set_offset(get_parent().get_offset() + (path_movement_speed*delta))
func update_children_animations(children, frame):
for child in children:
if child.get_type() == "Sprite":
child.set_frame(frame)
func _process(delta):
# update sprite animation frames and movement
if using_path:
# if the sprite is using a PathFollow2D node.
# Update the animation based on direction walking.
current_position = get_parent().get_pos()
if positions.size():
previous_position = positions[positions.size()-1]
if (int(current_position.y) > int(previous_position.y)):
# moving down
facing = "down"
elif (int(current_position.y) < int(previous_position.y)):
# moving up
facing = "up"
elif (int(current_position.x) > int(previous_position.x)):
# moving right
facing = "right"
elif (int(current_position.x) < int(previous_position.x)):
# moving left
facing = "left"
if (positions.size() > 2):
# clear the list after the length exceeds 3 to free up memory.
positions.clear()
positions.append(get_parent().get_pos())
if current_animation:
var starting_frame = animations[current_animation][facing]
var ending_frame = starting_frame + animations[current_animation]['columns']
var max_frame = animations[current_animation]['columns']
var frames = range(starting_frame, ending_frame) # calculate frames in animation
# update movement
if is_moving and not using_path:
var new_pos = Vector2(get_pos().x, get_pos().y)
if facing == "down":
new_pos.y += movement_speed
elif facing == "up":
new_pos.y -= movement_speed
elif facing == "left":
new_pos.x -= movement_speed
elif facing == "right":
new_pos.x += movement_speed
set_pos(new_pos)
temp_elapsed = temp_elapsed + delta
# update animation
if(temp_elapsed > animation_speed):
if is_moving:
# update animation while moving
if(last_index == max_frame):
set_frame(frames[0])
last_index = 0
update_children_animations(children, frames[last_index])
else:
if (last_index == 0):
# prevents frames[0] from being rendered twice after last_index is reset to 0.
last_index = 1
# print(current_animation, facing, frames) # debugging
set_frame(frames[last_index])
update_children_animations(children, frames[last_index])
last_index += 1
temp_elapsed = 0
else:
# set idle sprite animation
last_index = 0
set_frame(frames[last_index])
update_children_animations(children, frames[last_index])
func load_json_config(filename, dict):
# Load filename.json and copy contents to empty dict.
var file = File.new()
file.open(filename, file.READ)
var text = file.get_as_text()
dict.parse_json(text)
file.close()
func set_name(new_name):
# Sets both the class-member name value and Label (Name) node text.
name = new_name
name_node.set_text(new_name)
func _ready():
load_json_config("res://animations.json", animations)
# set the default sprite frame (facing down while idle).
set_process(true)
set_fixed_process(true)
var starting_frame = animations[current_animation][facing]
var ending_frame = starting_frame + animations[current_animation]['columns']
var frames = range(starting_frame, ending_frame) # calculate frames in animation
set_frame(frames[last_index])
update_children_animations(children, frames[last_index])
if name:
name_node.set_text(name)
func output_spritesheet_animation_offsets(images_per_row, amount_of_rows):
# For debugging and testing
for row_number in range(amount_of_rows):
var offset = images_per_row * row_number
print(offset)
So if you have a copy of GoDot, load up my script and include my JSON file to see it in action.
Again, this is to prove that this can be accomplished with little effort, and it came to my mind immediately. So I'm wondering why the devs here didn't think of this.
Heck I might have to create my own engine on-top of GoDot (with proper JSON files included) if the devs don't add this anytime soon.