Skip to content

Conversation

@jyoung4242
Copy link
Contributor

@jyoung4242 jyoung4242 commented Dec 7, 2025

This PR adds comprehensive serialization and deserialization capabilities to Excalibur's core engine components, enabling persistent storage and restoration of component state.

Changes Made
Core Component Serialization Framework

Added serialize()
Added deserialize()
Added toJSON()
added private helper methods

BodyComponent required a custom serialization method that overrides the default
ColliderComponent required a custom serialization method that overrides the default
GraphicsComponent required a custom serialization method that overrides the default

TransformComponent required a custom serialization method that overrides the default

Benefits

  • Enables game state persistence (save/load functionality)
  • Supports entity cloning with proper state duplication
  • Provides foundation for networking and data synchronization
  • Improves debugging by allowing component inspection

Cursory testing locally was favorable, but I busted this out i a vacuum

tested with all native Actor components, some could use default serialization
tested with custom component

@jyoung4242
Copy link
Contributor Author

before merging, would need documentation and testing updated, but its too early for that, i need people to look at this, IMHO its not a trivial change, although it doesn't break anything active

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Dec 7, 2025

Deploying excaliburjs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9d6baee
Status: ✅  Deploy successful!
Preview URL: https://395f7a56.excaliburjs.pages.dev
Branch Preview URL: https://serializablecomponents.excaliburjs.pages.dev

View logs

@cloudflare-workers-and-pages
Copy link

Deploying excalibur-playground with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6656ac6
Status:🚫  Build failed.

View logs

@chrisk-7777
Copy link
Contributor

i need people to look at this

I haven't deep dived the code yet, but a few high level comments.

Looking really good!

I could see a case for implements Serializable<T>, similar to implements Clonable<T>. Then the serializable class is obligated via the serializable contract to implement serialize(), deserialize(), and toJSON()

There is also potential for decorators for serializable attributes. But I've always been on the fence about decorators because it adds an undesirable level of indirection.

An example might be:

class BodyComponent extends Component implements Clonable<BodyComponent>, Serializable<BodyComponent> {

  @serializable
  friction: number;

  @serializable
  mass: number;
}

Actually, even writing that out now, I don't think decorators would be a good fit.

Btw, you don't know how hard it is for me to write serializable, instead of the correct way serialisable :)

@jyoung4242
Copy link
Contributor Author

@chrisk-7777 i don't hate that at all. I'm doing some research, because i'm trying to understand the patterns with implements Clonable, as the base Component class has clone on it..... reads stuff.... comes back...
okay, so the whole thing is it creates the 'contract' that forces one to implement the clone() method, and in doing so, we can force a contract for serializable too. Gotcha...

So, my question is then... why is the base class for clone() already defined on the Component class. Wouldn't the extend Component take care of that already? I'm asking out of ignorance here...

@chrisk-7777
Copy link
Contributor

chrisk-7777 commented Dec 8, 2025

@chrisk-7777 i don't hate that at all. I'm doing some research, because i'm trying to understand the patterns with implements Clonable, as the base Component class has clone on it..... reads stuff.... comes back... okay, so the whole thing is it creates the 'contract' that forces one to implement the clone() method, and in doing so, we can force a contract for serializable too. Gotcha...

So, my question is then... why is the base class for clone() already defined on the Component class. Wouldn't the extend Component take care of that already? I'm asking out of ignorance here...

Yea, so there is a difference between extends and implements. The good thing about this topic is its present in Java, C++, PHP too, so you will find a wealth of info on it.

I will butcher this, its a huge topic, so I'm just cherry picking the absolute critical parts for this convo.

  • extends the functionality would exist in the base class, and then the child (the one calling extends Foo) could optionally extend it, or reference it, and any consuming code could also reference that functionality
  • implements is just saying "I promise I will implement this functionality, to make sure I satisfy the contract - leave it to me, I got this".

The key difference for this convo is with interfaces (the implements variant) its up to that class to choose how it wishes to satisfy that promise (the contract). Broadly speaking too much inheritance is considered "bad". But as with everything, there are very valid reasons when to reach for it - Actor extending Entity makes complete sense.

This probably explains it better than I could: https://stackoverflow.com/questions/38834625/whats-the-difference-between-extends-and-implements-in-typescript

In the case of this PR, you could have an array of objects - some that do implement Serializable and some that don't.

export function isSerializable(obj: any): obj is Serializable {
  return (
    obj != null &&
    typeof obj.serialize === "function" &&
    typeof obj.deserialize === "function"
  );
}

for (const item of arr) {
  if (isSerializable(item)) {
    // we're both compile-time safe and runtime safe at this point
    console.log("Serializable:", item.serialize());
  } else {
    console.log("Not serializable - skip!", item);
  }
}

Another key diff in Typescript is a class may implement multiple interfaces but extend only one parent. (Interestingly some languages allow extending multiple, but that would be chaos.). Again the actor is a great example of leveraging this: https://github.com/excaliburjs/Excalibur/blob/main/src/engine/Actor.ts#L266

So in the example above class BodyComponent extends Component implements Clonable<BodyComponent>, Serializable<BodyComponent> we're declaring that the BodyComponent satisfies everything to be clonable, and also everything to be serializable.

@chrisk-7777
Copy link
Contributor

chrisk-7777 commented Dec 8, 2025

....and I missed your core question.

clone is present on both BodyComponent and Component mainly to cast the return shape to BodyComponent on the following line. Giving the return type from clone a more accurate type. (I assume, correct me if I'm wrong Erik).

  public clone(): BodyComponent {
    const component = super.clone() as BodyComponent;
    return component;
  }

This is just calling the parent clone method to do the heavy lifting from an implementation perspective.

This example is probably more confusing, because we're mixing extends Component which is sharing the base functionality, but also the implements Cloneable.

You could omit implements Cloneable from BodyComponent and it would still technically work. However, the type returned from .clone() in consumer land would be Component not BodyComponent because it would be using the T in Clonable<T> in Component, instead of the T in Cloneable<T> in BodyComponent.

Whew, I'm convinced that isn't explained well.

In the case of Serialisable I suspect you may not need a base class, just an interface.

@jyoung4242
Copy link
Contributor Author

that's a solid explanation, @chrisk-7777 . Thx,.... my only concern with forcing an implementation, is that i'm putting a 'default' serialize() and deserialize() on the component class, i suspect MOST custom components could just use the default. but complicated components 'can' override it with a custom serializer

@chrisk-7777
Copy link
Contributor

that's a solid explanation, @chrisk-7777 . Thx,.... my only concern with forcing an implementation, is that i'm putting a 'default' serialize() and deserialize() on the component class, i suspect MOST custom components could just use the default. but complicated components 'can' override it with a custom serializer

Ah yep, that totally makes sense, esp. when there is a lot of shared functionality 👍

@jyoung4242
Copy link
Contributor Author

I gonna re-attempt this concept tomorrow as a centralized 'serializer' for all of Ex as opposed to shoving this functionality into Entity and Components... getting a ton of circular arguements

@jyoung4242
Copy link
Contributor Author

Got the vitest spec file done tonight and pushed to the PR

@jyoung4242
Copy link
Contributor Author

@eonarheim this has been open awhile, is this something we're interested in?

@eonarheim
Copy link
Member

My bad, I'll review ASAP

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants