Playdate SDK with TypeScript

:wave:

I've been exploring using TypeScript to build my games (I work on the language, so it's good to dogfood) - it doesn't negate needing to learn Lua, but it does carry the torch of JS ruining touching everything. It uses TypeScriptToLua, my VS Code extension and I've been building out a definition file for the Playdate SDK (in the zip).

To test it all out, I've built a trivial-ish game of snake, which is enough to prove that the whole pipeline works.

If you want to give it a run - here's a zip of the working example (to actually develop in it, you will need to run yarn install - I included the output Lua files in there in case folks were interested in seeing the ts2lua results. )

12 Likes

I can't download that zip, permissions issue.

Heh, maybe abusing discord URLs isn't the best pattern, here's a copy on my S3
https://ortastuff.s3.us-east-1.amazonaws.com/playdate/snake-in-typescript.zip

1 Like

Joined a minute ago, first post that caught my interested, and boom – of course you find @orta here :slight_smile: Nice work!

1 Like

This is very exciting! I am personally a statically-typed sort of guy, so the flexibility of Lua sometimes gives me the creeps. Should there be much of a performance hit by coding in TypeScript, then cross-compiling* to Lua?

Would it be possible to cross-compile* TypeScript to C?

'* I'm not sure if "cross-compile" is the proper way to describe what you're doing.

I believe the term used is ... transpiling? Transforming one language to another. Anyway, very cool @orta!

There are TypeScript to C, um, transpilers: GitHub - sebbekarlsson/tscc: Typescript to C compiler (TSCC)

Yeah, transpiling is generally the "move sideways" word to describe these.

IMO: perf will not be as good as well written Lua, but probably good enough and you can still write in Lua as well as TypeScript if you have code which has to be very fast.

The perf trade-offs come from two places:

  • The language impedance mis-match. Things which are easy and common in JavaScript are not always in cheap Lua. TS2Lua offers polyfills for most common cases, which I use in a bunch of places. Things like array pushing, concating or removing items don't directly have Lua 1-liners. This is like JS bread and butter.

  • The TS2Lua translated Lua looks like it's optimized for large codebases and prefers globals over locals when it can. Which is regularly brought up as a perf hit in the Playdate chat. This could maybe be worked around at the ts2lua transpiler level as this is likely a design choice they made.

1 Like

Re: transpiling to C - folks are exploring. IMO the most relevant one comes from the makecode team called Static TypeScript, which is built for making games on micro-controllers. It's the TS language without JS quirks, so kinda dreamy TBH - I wrote my last experimental game in it and they did express an interest to me a few months ago about wanting to look at the Playdate, so I can try help you all connect if you're interested.

2 Likes

We would definitely be interested in talking to them. We are in a bit of a crunch right now, so I don't know how much attention we could devote to them at this moment… but medium- to long-term, the idea of a high-level language that might run with something closer to the performance of C would be really interesting to us! If they are interested, have them get in touch with me at greg@panic.com. Thank you!

3 Likes

Interestingly, I found this project that compiles TS via LLVM. Having some trouble compiling it for Apple Silicon but that could be interesting as a native LLVM vector.

Update: I was able to compile it but I had to tweak a couple of lines in a C++ file. :man_shrugging: Now I'm running into some LLVM issues (I think).

Update 2: This is bonkers and nothing is working. I think @orta's idea to transpile the TS to Lua is probably a better ROI.

1 Like

I spent a week or so digging into different TS -> C compilers, and went down the same rabbit hole you did @_a2.

TypeScriptToLua worked great, and I've been putzing with writing out the full API in type declarations. There are some useful compiler annotations that I've been taking advantage of, as well as custom types like LuaMultiReturn. Operator map types also make it possible to compile the custom operations from lua, like addition between vectors.

So far I have animators, geometry, and graphics ported, but not fully tested.

anim

Types have been great so far, like having animator:currentValue() be type based on which new was used to construct it. I'll see about pushing code up sometime soon!

Edit 1: Pathfinding is now working. Some off-by-1 errors in the port, which is kind of the name of the game for lua -> TS.
pathfinding

Edit 2: This is gridview.lua from the examples, which is grid view, nineslice, and timers. I need to make timers a little more exactly typed, they're tricky because they support a constructor with any number of arguments, which become parameters to the timer callbacks. Just some generics I need to sort out.
gridview

Edit 3: I was able to get multi-file TypeScript projects to compile using TypeScriptToLua's bundling. All Lua is jammed into a single file, and require is mocked to load code from within that bundle (like how JS bundlers work).
The next, big thing to bite off is going to be implementing some sort of OOP. TSTL exposes plugins, which let you create arbitrary AST nodes. TypeScript class definitions will need to be ported to the custom Playdate API's class stuff, i.e.

class Foo extends Bar {
    constructor() {
        super();
    }
    method() {
        this.x = 2;
    }
}

becomes

class('Foo').extends(Bar)
function Bar::init()
  Bar.super.init(self)
end
function Bar::method()
  self.x = 2
end

Anyway, here's the Level 1-1 example from the SDK, mostly ported to TypeScript. Instead of doing the OOP stuff, I just have functions that spit out an object that mocks the properties a sprite would have, like:

const Player = () => {
  // ...
  const underlyingSprite = playdate.graphics.sprite.new();
  return {
    moveWithCollisions: (p: playdate.Point) => {
        return underlyingSprite.moveWithCollisions(p);
    }
}

11

Edit 4: I have the first pass of OOP transformations working! The following .ts:

require('CoreLibs/sprites');
require('CoreLibs/object');

const sprite = playdate.graphics.sprite;

class Ball extends playdate.graphics.sprite {
    constructor() {
        super();
        this.setImage(playdate.graphics.image.new('img/brick'));
    }

    draw(x: number, y: number, width: number, height: number): void {
        playdate.graphics.drawCircleAtPoint(x, y, 100);
    }
}

const x = new Ball();
x.add();
x.moveTo(100, 100);

playdate.update = () => {
    sprite.update();
};

export {};

compiles to

import("CoreLibs/sprites")
import("CoreLibs/object")
local sprite = playdate.graphics.sprite
class("Ball").extends(playdate.graphics.sprite)
Ball.init = function(self)
    Ball.super.init(self)
    self:setImage(playdate.graphics.image.new("img/brick"))
end
function Ball.draw(self, x, y, width, height)
    playdate.graphics.drawCircleAtPoint(x, y, 100)
end
local x = Ball()
x:add()
x:moveTo(100, 100)
playdate.update = function()
    sprite:update()
end
return ____exports

which in turn draws a little brick on the screen.

My guess is that my transformations are pretty fragile. I know I don't support class properties yet, and I'm not yet sure how I'll approach things like statics.

9 Likes

How did you get this to work? I wrote my own playdate.ts plugin that I was working off of. I didn't know if anyone else would be interested in this. I'm not sure if Playdate's "flavor" of Lua works with the return ____exports thing so I tried to skirt around that. DM me if you want to collaborate on this somehow.

Hi Alex!

Playdate's Lua works with TSTL's ____exports business! In the bundle TSTL makes, it mocks the require function to load the exports from the table it builds up. I'm using @orta 's TSTL plugin, which transforms require() calls to import() calls, then TSTL turns TypeScript's import statements into require.

I can share my code, both for my TSTL and my type definitions. I have the entire API in a playdate.d.ts, although I have found some bugs while porting example projects so it's certainly not perfect.

I'm away from that computer this weekend, but I'll share some .zips on here on Monday, most likely! Eventually I'd like to get the types and the plugin into npm, but it's still just something I'm mucking around with when I have a spare moment. I'll try to get the types and plugins on GitHub soon so anyone can contribute.

Okeydoke, I lumped my playdate.d.ts and TSTL plugin into a single directory and just zipped it all up. You should be able to npm i && npm run build and have a working pdx.

Let me know if it doesn't work or if you have questions or suggestions. I'll try to figure out a plan for getting this up on GitHub or something, like I mentioned.

PlaydateTSExample.zip (26.4 KB)

1 Like

Do you guys have any updates on this? Like a Github repository?

The types declarations from Andy seems pretty complete.

Hey Andy, not sure if you're still working on this but I'd love if you spun up a GitHub project for your work. I'm interested in using TS for PlayDate and would definitely prefer to contribute to a shared set of types rather than fork off my own. Let me know if / when you create a repo!

I'm still kind of poking around on it, yes! My plugin broke with the latest version of TypescriptToLua, but once I fix that I'll try to get something live. I've been working on a little game in TS as a way to work on this, and that's been keeping me interested in it :slight_smile:

Awesome! I can definitely saying having TypeScript in my toolkit is definitely going to make me 1,000% more likely to make a PlayDate game, so I'm excited.

Ok, here's a dump of what I've been working on with this.

First, my work on this doesn't represent Panic. This is just something I’ve been doing in my free time for fun.

As a quick summary, I'm developing for Playdate in TypeScript using TypeScriptToLua. TSTL transpiles TypeScript to Lua, and given a TypeScript declaration file that defines a Lua API it can produce Lua that calls that API.

TSTL also supports plugins, which allows for custom code transformations. This is useful for weird specifics of some Lua runtimes. @orta wrote a great, concise plugin for using import rather than require, and I’ve expanded on that to write an apparently functional plugin for Playdate’s custom OOP stuff. As a caveat, my plugin doesn't yet handle class definitions as exports (export default class Blah {}). If you want to export a class from a module, define it first then export it separately. Also, the code is a mess.

After all that, here’s an example of some TypeScript and the Lua it produces.

require(“CoreLibs/graphics”);
require(“CoreLibs/sprites”);
require(“CoreLibs/object”);
class Ball extends playdate.graphics.sprite {
  radius = 10;
  pos = {
    x: 40,
    y: 20,
  };
  velocity = {
    // initial horizontal velocity
    dx: 8,
    dy: 0,
  };
  acceleration = {
    dx: 0,
    // gravity
    dy: 2,
  };
  constructor() {
    super();
    this.setBounds(0, 0, 20, 20);
    this.setCenter(0.5, 0.5);
  }
  update() {
    this.velocity.dx += this.acceleration.dx;
    this.velocity.dy += this.acceleration.dy;
    const x = this.pos.x + this.velocity.dx;
    const y = this.pos.y + this.velocity.dy;
    if (x - this.radius < 0 || x + this.radius > playdate.display.getWidth()) {
      this.velocity.dx *= -1;
    } else {
      this.pos.x = x;
    }
    if (y - this.radius < 0 || y + this.radius > playdate.display.getHeight()) {
      this.velocity.dy *= -1;
    } else {
      this.pos.y = y;
    }
    this.moveTo(this.pos.x, this.pos.y);
  }
  draw(x: number, y: number, width: number, height: number) {
    playdate.graphics.drawCircleAtPoint(this.radius, this.radius, this.radius);
  }
}
const ball = new Ball();
ball.add();
playdate.update = () => {
  playdate.graphics.sprite.update();
};

becomes...

-- a bunch of bundling stuff and library code
--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
import(“CoreLibs/graphics”)
import(“CoreLibs/sprites”)
import(“CoreLibs/object”)
class(“Ball”).extends(playdate.graphics.sprite)
Ball.init = function(self)
    Ball.super.init(self)
    self.radius = 10
    self.pos = {x = 40, y = 20}
    self.velocity = {dx = 8, dy = 0}
    self.acceleration = {dx = 0, dy = 2}
    self:setBounds(0, 0, 20, 20)
    self:setCenter(0.5, 0.5)
end
function Ball.update(self)
    local ____self_velocity_0, ____dx_1 = self.velocity, “dx”
    ____self_velocity_0[____dx_1] = ____self_velocity_0[____dx_1] + self.acceleration.dx
    local ____self_velocity_2, ____dy_3 = self.velocity, “dy”
    ____self_velocity_2[____dy_3] = ____self_velocity_2[____dy_3] + self.acceleration.dy
    local x = self.pos.x + self.velocity.dx
    local y = self.pos.y + self.velocity.dy
    if x - self.radius < 0 or x + self.radius > playdate.display.getWidth() then
        local ____self_velocity_4, ____dx_5 = self.velocity, “dx”
        ____self_velocity_4[____dx_5] = ____self_velocity_4[____dx_5] * -1
    else
        self.pos.x = x
    end
    if y - self.radius < 0 or y + self.radius > playdate.display.getHeight() then
        local ____self_velocity_6, ____dy_7 = self.velocity, “dy”
        ____self_velocity_6[____dy_7] = ____self_velocity_6[____dy_7] * -1
    else
        self.pos.y = y
    end
    self:moveTo(self.pos.x, self.pos.y)
end
function Ball.draw(self, x, y, width, height)
    playdate.graphics.drawCircleAtPoint(self.radius, self.radius, self.radius)
end
ball = Ball()
ball:add()
playdate.update = function()
    playdate.graphics.sprite.update()
end
 end,
}

and the program running:
bouncy

Here’s a zip of the project, including the TSTL plugin and TS declaration file for the Playdate Lua API. Assuming you have node and npm installed, you should be able to run npm install then npm run build to generate a Source.pdx you can run in the simulator or on device.
tstlexport.zip (23.2 KB)

In my local development I have my TSTL plugin and .d.ts type declaration as separate packages that I symlink with npm link, but I’ve just tossed them in here to share. I haven’t had the momentum to publish either on npm, but I welcome anyone to use that code however they like, including publishing it as a package if you want.

The TypeScript declaration was just made from me copying the Lua API while watching TV or whatever, and is not based on any proprietary information. Some of it is likely wrong; I make corrections to it when I find something broken, but I haven’t written enough TypeScript to cover the entire giant Playdate API.

I’m working on a larger game in TypeScript (again, personal project, doesn't represent Panic) which is making me think about the performance of the transpiled Lua. The first half of this post talks about loops: Fewer milliseconds.md · GitHub

Hopefully this is enough to get anyone started developing in TypeScript.

4 Likes

Here's some more about that game in progress, specifically quickly drawing a grid of sprites: Fast Roguelike Drawing.md · GitHub

2 Likes