This document contains notes about the files that collectively make up a GameMaker project. It is incomplete and may not be up to date, so it should be considered a starting point to understand how projects are put together.
Since Stitch needs to read, update, and create project files, being able to accurately determine the structure of a given file's content is essential.
Data structures can vary at multiple levels, and it is not clear if there is always a way to determine for sure what the structure is supposed to be for any given IDE version.
- In
yy
andyyp
files, their are nested resources (objects) that can each have their ownresourceType
andresourceVersion
fields. - A change to the structure of a subresource is not paired with a change to the
resourceType
orresourceVersion
of the parent. - A change to a resource may be reflected by a change to either, both, or none of the
resourceType
andresourceVersion
fields. - A change to the root (
yyp
)resourceVersion
field at the same time as a change to a subresource's structure might imply that any project with that version or greater will have the same subresource structure, but we assume that these are uncoupled. - A change to the structure of some subresource is always coupled to a new IDE version, but it is not clear how other IDE versions relate to that. For example, if the Beta channel
2022.800.x.y
release changes the Sprite.yy
file structure, it's not clear if that also means that stable2022.8+
will have that same change. Nor is it clear if beta2022.900+
will not revert to the prior structure.
Given the uncertainty in determining appropriate data structures for any given project version, Stitch must take a conservative approach to make sparse changes and lean on GameMaker to auto-fix outdated structures.
- We work sparsely on project files -- only changing fields that we have a reason to change, and leaving everything else as-is.
- We create project files with sparse fields -- only fields that are required or for which we have initial data to provide.
- Where there is uncertainty we fall back on older resource versions since GameMaker is likely to be able to convert them on load.
- We assume variations are independent (when that can make sense), making inferences per field we need to do something about
- We assume that a resource (any object with a
resourceType
field) version (the siblingresourceVersion
field)
- The
.yyp
resource version bumped to1.6
- The "Windows UWP" and "Xbox One" platforms were removed from the
.yyp
Options
array. - Across all
.yyp?
files,tags
fields whose values were an empty array were automatically purged by GameMaker.- Experiments with 2022.600.0.153 showed that these fields were optional in prior versions
- Sprite
.yy
files had significant changes, but the overall data structure stayed intact.- In general, some combination of the
resourceType
andresourceVersion
fields were changed for subresources that changed. - The exception was
sprite.yy#/sequence
, which had two fields removed but did not have a bump in version. - On load, GameMaker immediately converted the existing sprites to the new format.
- In general, some combination of the
- The Stitch behavior changes that accomodates these file changes in the safest way are:
- Mark
tags
fields as optional in the types - Skip the
tags
fields when creating new resource files - Before creating or updating a sprite, fetch an existing sprite that contains >0 frames
- Use the sample sprite to infer the correct subresource type
- Operate on a per-field basis to make minimum assumptions
- If no sample is available, use the older format (this is an edge case, and this solution is least likely to break anything permanently)
- Use the sample sprite to infer the correct subresource type
- Mark
The main organizational content of a GameMaker project is in JSON-like files,
typically with extensions .yyp
or .yy
. In GMS2.3 these files have trailing
commas (meaning standard JSON parsers cannot read them) and can have Int64 values
(depending on parsing language, meaning data will be lost during save/load).
Each resource, of any type, typically includes a .yy
file and may also include
files of various resource-specific types.
- "Groups" (folders) in GMS2.3 do not also create folders in the local file structure.
- Every resource ends up in a flat file structure inside a per-resource-type root folder.
- There is one file folder per resource type.
- Organization of resource files within each resource folder seems to be unchanged relative
to GMS2.2. Mostly consists of
type/name/name.yy
, sometimes with additional files of various types. - Which folder an asset is in is determined by that asset's
.yy
file via the"parent"
field.
Examples:
// For assets in the root level (no parent folder)
// (Just refers to the project itself)
"parent": {
"name": "sample-project",
"path": "sample-project.yyp",
},
// For assets in folders
// (The "name" is the folder name)
// (The "path" is to a non-existent file and the root is always "folders";
// dropping ^folders and .yy$ yields the visible path in the tree )
// (Exactly matches name/path in an entry in the .yyp file)
"parent": {
"name": "level3",
"path": "folders/sample_resources/level2/level3.yy",
},
In GMS2.3 resources, folders, etc can have tags. These are used for filtering content in the resource tree. They are always present as a "tags" field with a simple list of strings.
Colors are not saved. You can color-code assets, but these are only stored at the IDE level and therefore are not accessible by Stitch.
GMS2 projects use Globally Unique Identifiers (GUIDs) -- also called UUIDs -- in some places when unique identifiers are needed but user-provided names are not available.
We don't know what algorithm is used to generate these identifiers, but all our experiments show that as long as they are dash-separated hex strings with the exact same number of characters as GUIDs created by GMS2 itself they'll work just fine. We can use either random UUIDs (v4) or generate them based on well-defined parameters (v3), depending on context.
Each game project has a root *.yyp
file that describes all of its resources
and some of its high-level metadata. This is the entry point for GameMaker projects.
The root contents of the GMS2.3 yyp
file are these:
resources
: Lists resources of every type except for folders, options, included files, texture groups, and audio groups- example entry:
{"id":{"name":"object","path":"objects/object/object.yy",},"order":4,}
- (note the complete absence of IDs)
- (note trailing commas)
- (note the use of POSIX-style path separators)
- example entry:
Options
: Example{"name":"Amazon Fire","path":"options/amazonfire/options_amazonfire.yy",}
configs
: Example:
{
"name": "Default",
"children": [
{ "name": "steam", "children": [] },
{ "name": "ps5", "children": [] }
]
}
RoomOrderNodes
:[{"name":"the_room","path":"rooms/the_room/the_room.yy",}]
Folders
:{"folderPath":"folders/sample_resources/level2/level3.yy","order":1,"resourceVersion":"1.0","name":"level3","tags":[],"resourceType":"GMFolder",}
- (resourceType is always GMFolder)
- (resourceVersion is always "1.0")
AudioGroups
: Example{"targets":461609314234257646,"resourceVersion":"1.0","name":"audiogroup_default","resourceType":"GMAudioGroup",}
- (always type "GMAudioGroup")
- (always "resourceVersion":"1.0")
- The targets list is binary mask of some sort identify platforms
TextureGroups
: Example{"isScaled":true,"autocrop":true,"border":2,"mipsToGenerate":0,"targets":461609314234257646,"resourceVersion":"1.0","name":"Default","resourceType":"GMTextureGroup",}
- (always type "GMTextureGroup")
- (always "resourceVersion":"1.0")
- The targets list is binary mask of some sort identify platforms
IncludedFiles
: Example{"CopyToMask":-1,"filePath":"datafiles/level_1","resourceVersion":"1.0","name":"discoverable_datafile.txt","resourceType":"GMIncludedFile",}
- (always type "GMIncludedFile")
- (always "resourceVersion":"1.0")
- "CopyToMask":-1 means "deploy to all targets". We use different masks based on config and target platform.
Resources are found in resourceType/resource_name
folders, always with a file
called resourceType/resource_name/resource_name.yy
. These are JSON files whose contents
vary by asset type, but that have the common fields:
{
"name": "resource_name",
"resourceType": "GMShader",
"tags": [],
// Location in the visible heirarchy (not on disk)
"parent": {
"name": "sample_resources",
"path": "folders/sample_resources.yy"
},
"resourceVersion": "1.0" // constant
}
Each asset type may include additional files and folders besides the .yy
file.
Naming conventions for these other files varies by resource type.
Sprites are the most complex content within the GMS2 project file structure.
As with all other resources all their data is defined in a .yy
file.
A brand new sprite (sampleSprite
), without adding a custom image or changing any details,
results in the following files:
sprites/sampleSprite/
sprites/sampleSprite/sampleSprite.yy
sprites/sampleSprite/{frameId1}.png
Composite image (actually rendered by the game)sprites/sampleSprite/layers/
sprites/sampleSprite/layers/{frameId1}/
sprites/sampleSprite/layers/{frameId1}/{layerId1}.png
(frameId1
, layerId1
, etc are placeholders for random GMS2-generated GUIDs.)
For details on what each of these fields mean and their allowed values, see the Typescript typings.
Using the "Import" option in a sprite causes all image files to be replaced and all GUIDs to be recomputed.
Adding a new subimage via the Editor leaves the original GUIDs intact and simply adds the new one.
Sorting subimages via the Editor has no impact on the file
structure, but does change the .yy
file: the objects in the
frames
array are are sorted to reflect the order seen in
the editor, as are those in sequence.tracks.keyframes.Keyframes
.
Adding a new layer creates a new GUID for that layer, with
correspondingly-named images in each layers/{frameId}
folder.
Creating a "Layer Group" only changes content in the .yy
file: the new group is added to the layers
array basically
as another layer, but with the added "layers" field that
is another array containing the nested layers. The layer
group does have a GUID, but it does not appear to be used
anywhere else.
A complex sprite (sampleSprite
) with multiple subimages,
multiple layers, and a layer group results in the following
file structure:
sprites/sampleSprite/
sprites/sampleSprite/sampleSprite.yy
sprites/sampleSprite/{frameId1}.png
sprites/sampleSprite/{frameId2}.png
sprites/sampleSprite/{frameId3}.png
sprites/sampleSprite/layers/
sprites/sampleSprite/layers/{frameId1}/
sprites/sampleSprite/layers/{frameId1}/{layerId1}.png
sprites/sampleSprite/layers/{frameId1}/{layerId2}.png
sprites/sampleSprite/layers/{frameId2}/
sprites/sampleSprite/layers/{frameId2}/{layerId1}.png
sprites/sampleSprite/layers/{frameId2}/{layerId2}.png
sprites/sampleSprite/layers/{frameId3}/
sprites/sampleSprite/layers/{frameId3}/{layerId1}.png
sprites/sampleSprite/layers/{frameId3}/{layerId2}.png
(frameId1
, layerId1
, etc. are placeholders for random GMS2-generated GUIDs.)
Each sprite .yy
file includes a sequence
field, which has the exact
same structure as the .yy
file of an independent Sequence resource.
This represents the default animation for a sprite, where the frames
are simply looped over.
In our experiments it does not seem that there is a case where these sequences living inside a sprite resource are changed, with the following exceptions:
playbackSpeed
andplaybackType
are changed when the "FPS" field in the sprite editor is modified.length
reflects the current number of framestracks.keyframes
reflects the current frames and their order (one keyframe object per frame).
Sprites can be used in Sequence Resources, and their frames can be directly reference by those sequences. The references are by index only, so the GUID of the linked frames is not used and any changes to frames (re-ordering, deletion, or addition) are not reflected by GameMaker in changes to the code/resources that refer to them.
For example, if a sprite has two frames, a sequence refers to frame1 of that sprite, and then we delete frame1 via the GMS2 editor, the sequence still refers to the same index position as before. GameMaker allows this, at least in the editor, with out-of-bounds indexes falling back on the last existing frame.
Collectively, this means that GameMaker does not dynamically track references to sprite frames in any way, and any change that causes a specific image to have a different index will cause all references to that image to be referencing a different image now.
For each sprite created in spine there are typically 3 exported
files. For a sprite named mySprite
:
mySprite.png
The file containing all sprite frames.mySprite.json
The JSON file describing the bones and animations.mySprite.atlas
A YAML-like file describing where the frames are in the Atlas.
(When importing manually via the GMS2 IDE, you will only see the PNG
and JSON files in the file chooser. However, if the .atlas
file is
missing, then import will fail.)
There can be additional source .png
files, though it's currently unclear what the scenario is wherein this occurs.
The core of a Spine sprite is the same as a regular sprite. There are a few differences:
- While a layer, with a random
layerId
, is listed in the.yy
file, it does not actually need to exist as an image/folder. - No "composite" images are required
- The spine asset is given a random GUID that we'll call
spineId
- The atlas and JSON files are stored in the root, with names
changed to the
spineId
(keeping their extensions) - There is a PNG frame also named after the
spineId
, however it is a composite that GameMaker uses to display to the user in the Sprite editor and sprite thumbnail. It does not need to be created by tools, since GameMaker will auto-generate it
- The atlas and JSON files are stored in the root, with names
changed to the
- The original
.png
(s) are copied as-is into the root of the sprite's resource folder, without changing their names.
GameMaker 2.3 does not support current versions of Spine. The Spine JSON file export for the supported version is typed in types/Spine.ts.
Note that GameMaker completely ignores audio files associated with a Spine file, so sounds need to be separately managed.
All we have to do to create a sprite resource from Spine exports is:
- Create a new sprite as usual. Defaults are all totally fine. Can use the same 64x64 blank image that GameMaker uses.
- Copy the spritesheet into the sprite's root without changing the name.
- Create a
spineId
- Copy the .atlas and .json files over to the root, renaming them with
the
spineId