BiwaScheme: Lisp in PlayCanvas!

Hey, everyone!

My brother and I are huge fans of Lisp, and we decided to give implementing BiwaScheme into PlayCanvas a shot!

We wrote a small wrapper script that will run BiwaScheme scripts once on initialization and will repeat any code in the Scheme update function as javascript code during run-time, thanks to a really handy function written by Yutaka Hara, so it shouldn’t cause any overhead!

A couple things could probably be handled better, for example although we initially intended for you to use schemeRunner.js for all of your game objects, we realized a little too late that this didn’t create much flexibility in running multiple Scheme files on one object, as well as the handy feature of dragging and dropping assets onto a script’s attributes, so we kind of took the lazy way out of both of those problems and just created duplicates of the same runner script with the attributes we needed.

It works though! We re-wrote the entire example ball game in BiwaScheme for you guys to get an idea of the workflow if you’re interested!

Feel free to ask questions or make any improvements as you see fit!

I think by far the most commented Scheme script is teleportable.scm, so I would look at that first. The original scripts are also there for you to compare and contrast. They’re mostly one to one, although we got rid of the “to” and “from” variables in teleport. We could add those in pretty easily, but I think it’s pretty obvious how to go about it from the rest of the code anyway.

Thanks for reading!

Edit:

R3 update:

Comes with a single schemeCacher.js file that you can use to define the script names and attributes necessary for your SCM file.

Scheme code is interpreted by a static scheme.js file and assigned back to the gameobject that called it as javascript code during run-time.

As always, feel free to leave any suggestions!

https://playcanvas.com/project/647301/overview/lisp-ball-2-example

2 Likes

This is great, congrats! I also love functional languages, though don’t get much time to use it in practice.

Did you consider using a scheme->js cross-compile approach instead of interpreter? Is it possible? I wonder if compiling the scheme sources to JS would give you the benefits of using a functional language, but still run quickly and also integrate more easily into the existing playcanvas framework?

Looking at your example, you could simplify things by moving the initialize() and update() functions in runner/runnerCamera/runnerMovement to their own script (for example scheme.js). Then you could just call the functions from the scripts that need it (passing in this). You would have to make sure scheme.js is loaded first in the scene settings panel, but with this in place adding new entities should be quite simple. Just call SchemeInitialize(this) (or whatever you call the function) and SchemeUpdate(this).

2 Likes

If you want syntax highlighting, you can open F12/devtools and instruct CodeMirror to use it:

CodeMirror.defineMode("commonlisp", function (config) {
  var specialForm = /^(block|let*|return-from|catch|load-time-value|setq|eval-when|locally|symbol-macrolet|flet|macrolet|tagbody|function|multiple-value-call|the|go|multiple-value-prog1|throw|if|progn|unwind-protect|labels|progv|let|quote)$/;
  var assumeBody = /^with|^def|^do|^prog|case$|^cond$|bind$|when$|unless$/;
  var numLiteral = /^(?:[+\-]?(?:\d+|\d*\.\d+)(?:[efd][+\-]?\d+)?|[+\-]?\d+(?:\/[+\-]?\d+)?|#b[+\-]?[01]+|#o[+\-]?[0-7]+|#x[+\-]?[\da-f]+)/;
  var symbol = /[^\s'`,@()\[\]";]/;
  var type;

  function readSym(stream) {
    var ch;
    while (ch = stream.next()) {
      if (ch == "\\") stream.next();
      else if (!symbol.test(ch)) { stream.backUp(1); break; }
    }
    return stream.current();
  }

  function base(stream, state) {
    if (stream.eatSpace()) {type = "ws"; return null;}
    if (stream.match(numLiteral)) return "number";
    var ch = stream.next();
    if (ch == "\\") ch = stream.next();

    if (ch == '"') return (state.tokenize = inString)(stream, state);
    else if (ch == "(") { type = "open"; return "bracket"; }
    else if (ch == ")" || ch == "]") { type = "close"; return "bracket"; }
    else if (ch == ";") { stream.skipToEnd(); type = "ws"; return "comment"; }
    else if (/['`,@]/.test(ch)) return null;
    else if (ch == "|") {
      if (stream.skipTo("|")) { stream.next(); return "symbol"; }
      else { stream.skipToEnd(); return "error"; }
    } else if (ch == "#") {
      var ch = stream.next();
      if (ch == "(") { type = "open"; return "bracket"; }
      else if (/[+\-=\.']/.test(ch)) return null;
      else if (/\d/.test(ch) && stream.match(/^\d*#/)) return null;
      else if (ch == "|") return (state.tokenize = inComment)(stream, state);
      else if (ch == ":") { readSym(stream); return "meta"; }
      else if (ch == "\\") { stream.next(); readSym(stream); return "string-2" }
      else return "error";
    } else {
      var name = readSym(stream);
      if (name == ".") return null;
      type = "symbol";
      if (name == "nil" || name == "t" || name.charAt(0) == ":") return "atom";
      if (state.lastType == "open" && (specialForm.test(name) || assumeBody.test(name))) return "keyword";
      if (name.charAt(0) == "&") return "variable-2";
      return "variable";
    }
  }

  function inString(stream, state) {
    var escaped = false, next;
    while (next = stream.next()) {
      if (next == '"' && !escaped) { state.tokenize = base; break; }
      escaped = !escaped && next == "\\";
    }
    return "string";
  }

  function inComment(stream, state) {
    var next, last;
    while (next = stream.next()) {
      if (next == "#" && last == "|") { state.tokenize = base; break; }
      last = next;
    }
    type = "ws";
    return "comment";
  }

  return {
    startState: function () {
      return {ctx: {prev: null, start: 0, indentTo: 0}, lastType: null, tokenize: base};
    },

    token: function (stream, state) {
      if (stream.sol() && typeof state.ctx.indentTo != "number")
        state.ctx.indentTo = state.ctx.start + 1;

      type = null;
      var style = state.tokenize(stream, state);
      if (type != "ws") {
        if (state.ctx.indentTo == null) {
          if (type == "symbol" && assumeBody.test(stream.current()))
            state.ctx.indentTo = state.ctx.start + config.indentUnit;
          else
            state.ctx.indentTo = "next";
        } else if (state.ctx.indentTo == "next") {
          state.ctx.indentTo = stream.column();
        }
        state.lastType = type;
      }
      if (type == "open") state.ctx = {prev: state.ctx, start: stream.column(), indentTo: null};
      else if (type == "close") state.ctx = state.ctx.prev || state.ctx;
      return style;
    },

    indent: function (state, _textAfter) {
      var i = state.ctx.indentTo;
      return typeof i == "number" ? i : state.ctx.start + 1;
    },

    closeBrackets: {pairs: "()[]{}\"\""},
    lineComment: ";;",
    blockCommentStart: "#|",
    blockCommentEnd: "|#"
  };
});

CodeMirror.defineMIME("text/x-common-lisp", "commonlisp");

cm = editor.call('editor:codemirror')
cm.setOption("mode", "commonlisp")
2 Likes

You know, I decided to give those “SchemeInitialize” and “SchemeUpdate” functions a shot and for some reason having it as a static function in scheme.js is making it lose scope half-way through evaluating it.

Mid-way through the script it throws the error that “txt.replace”, one of BiwaScheme’s internal functions, is not defined.

Not really sure why that is, since the interpreter is already defined by using the BiwaScheme static and making it a new BiwaScheme interpreter class, it itself is the object calling the evaluate function.

I’ll keep trying some different things. Thank you for the suggestions!

@kungfooman
Thank you, that’s pretty cool! I gotta find a way to have it run that script automatically, it’s nicer than copying and pasting between the text editor.

1 Like

I got it, must’ve been something I was doing.

Slightly cleaner version is up!

@codebon
I think you were thinking more along the lines of having the scheme.js file add it to a kind of conglomerate script at run-time so that init and updates are all done at once, but the way I handled a lot of problems is still pretty object oriented, so although they all call the same static functions for interpretation, they’re assigned back to the gameobjects after processing.

1 Like

Revision 3 is up. After looking at how the Seemore project handled their FPSView.JS, I realized a single javascript file can define multiple scripts.

So using this, you can add any attributes and script names you’d like to use to a single schemeCacher.js.

This means you don’t need a JS file for every SCM file you write, and you also now have access to the PlayCanvas attribute system. You can see how it works using the same link above

Thanks for reading, and let me know if you have any questions!

1 Like

My brother and I have been using R3 pretty extensively for a new game project, and we’re pretty happy with the workflow! If you’re looking to enter a Lisp gamejam or just learn the language in general, give it a try!

We recently wrote an FPS controller with mouse look, jumping, and crouching!

gameplay

Might make a tutorial on it after the project is all done!

Thanks for reading, and let us know if you have any questions!

3 Likes