seeking feedback on new lightweight scripting language for use in Unity games

Hello all,

I’ve been working for the past month or two on a new scripting language (“MiniScript”) designed especially for use within Unity. This would be something you embed into your game, perhaps because

  • you want an easy language for modding your game
  • you’re making a programming game (like the classic RoboWar, etc.)
  • you want to be able to change even the code of your game via Asset Bundles

Or maybe for other reasons I haven’t thought of! I’ve looked at the other alternatives — primarily Lua, or using the mono runtime compiler — but wasn’t satisfied with the compromises they make. So I designed MiniScript to be my dream language: trivial to embed, easy to learn and use, efficient, low-overhead multitasking, etc. It will also work on all platforms, including mobile.

I’m now to the stage where I would like some feedback from you all on the design of the language (and if you happen to find any bugs, bug reports will be appreciated too!). Note that the language is not finished; I have a few more tweaks in mind myself, and expect to incorporate much of the feedback I receive here. But it’s up and running, and in good enough shape to start sharing it with the world.

So, if this sounds interesting to you, please go check out the web demo. Or, to get a very quick look at the language, read this one-page MiniScript Quick Reference.

Please kick the tires, take it for a spin, and let me know what you think!

Thanks,

  • Joe
3 Likes

In general it feels okay. Major kudos for implementing your own language. Couple of quick points

  • It fails silently quite a bit. Reminds me of JavaScript in that regard. Simply stops working without any indication why.
  • Chaining the return value of a function doesn’t work. I can’t do MyType.MyFunction.x
  • Literals cannot begin with a . (ie .99 must be 0.99). This is mostly an inconvenience.

I was going to give you a nice little random wander script. But I found a breaking bug. :frowning: There seems to be some leakage between objects? It seems like if I have two instances of the same class around then calling a method on one is changing values on the other. Or something weird like that. I tried to isolate it for you.

Here is the simplest sample code I could produce to replicate the error:

Vector2 = {"x":0,"y":0}

Vector2.Print = function ()
    print ("x: " + self.x + " y: " + self.y)  
end function

Vector2.AssignOneToY = function ()
    returnValue = new Vector2
    returnValue.x = self.x
    returnValue.y = 1
    return returnValue
end function

myVector = new Vector2
myOtherVector = new Vector2

myVector.x = 50
myVector.y = -50

myVector.Print
myOtherVector.Print

myOtherVector = myOtherVector.AssignOneToY
myVector = myVector.AssignOneToY

myVector.Print
myOtherVector.Print

The expected output is
x: 50 y: -50
x: 0 y: 0
x: 50 y:1
x: 0 y:1

Instead I get this
x: 50 y: -50
x: 0 y: 0
x: 50 y:1
x: 50 y:1

1 Like

Hey @Kiwasi , thanks for taking the time to try it so thoroughly and provide such useful feedback!

Could be; it’s already stricter than, say, Lua, which is one of my complains about that language — for example, an undefined identifier or invalid map key will throw an error in MiniScript, where it just evaluates to null in Lua. But it wouldn’t surprise me that there are other other cases that still fail silently. This is something I hope to improve.

Confirmed. Should be fixable.

Doh! Easy fix.

Thanks so much. It appears to actually be a matter of creating a new object in a method as a local variable, and returning it. Somehow it is returning the same object multiple times, rather than creating a new one each time. As you say, it’s weird! But I’ll get to the bottom of it, thanks to your excellent demo case.

Oh, and that reminds me: I have a pretty long integration test suite which might also serve as more example code. I’ve added it to the web page.

OK, the problems described above should all be fixed now.

The weird bug turned out to be: list/map literals, as well as the “new” operator, were creating their objects only once, at parse time. So every time the code executed, you got back the same object. That’s great for immutable stuff like strings and numbers, but exactly wrong for mutable objects, as you found. But it would only be noticeable in loops, or functions that create such an object, where you can actually execute the same code more than once.

The hardest fix was the chaining of calls… I had to swizzle around my parse tree a bit to get everything happening in the right order. But it seems to be working now. (I’ve updated the web demo.)

Thanks again for the feedback, and I look forward to what you (everyone) think on future tries!

1 Like

Looks cool, although I didn’t test thoroughly enough to provide the kind of feedback that @Kiwasi did.

PS In reference to your Product test case comment you can aggregate function calls across a collection using linq:

list.Aggregate((a, v)=> a * v);

PPS Some kind of yield like capability would be handy so we can animate stuff (if not already there).

Huge props for actually doing it! More often than folks yammer on for paragraphs about some ‘idea’ they have but never actually doing anything.
“I have an idea…” < “I built this thing…”

I haven’t played with it yet, but it looks cool. It could be used for some interesting things. Great work.

2 Likes

There is the wait command. One of the nifty things about this language is its not subject to infinite loops the way C# in Unity is. I have no idea how the concept of a frame is handled, and it seems like it would get messy for a complicated system. But for simple non programmers it might do alright.

1 Like

Sounds like what I’m looking for will check it out when I get a chance.

Nice!

I know some other folks who made an app for iOS a while ago that’s all about scripting and programming, and they did get their app up and approved but they also had to make some compromises with Apple to make it happen. I think it boiled down to satisfying Apple that it couldn’t be used to subvert their approval process (ie: any new code had to be explicitly entered by a user), but I’m not 100% sure.

Anyway, I know that’s not what you’re going for here, I just don’t know where Apple draws the line so it’s worth being aware of it.

2 Likes

Yes, @Kiwasi is exactly right — in fact if you go to the demo, and run any of the examples, they all do animation (using wait). There is also a “time” function which is basically the same thing as Time.time in Unity, so you can use that to keep your speed constant despite whatever else might be going on.

I didn’t include anything specifically related to frames, because in general, MiniScript doesn’t have to be used in a frame-dependent way. I think of it more like: the host app kicks off one or more scripts, and gives them time as it’s able (you can tell a MiniScript to run for no more than N milliseconds). The demos work by letting the scripts update the “ship” map (containing x, y, and rot) whenever they like, and then there’s a MonoBehavior that just pulls those values out of the script context, and updates the actual ship sprite on each frame.

But I can see how for some uses, being able to tie things to the frame update would be very handy. I can see two ways to approach that:

  • The host app invokes a function on each frame… and if it doesn’t return in a reasonable amount of time, I guess we just have it continue on the next frame until it’s done, and then repeat. OR,
  • We invoke the script just once, and pull state out of global variables as in the current demo, but add some sort of “waitForNextFrame” function that blocks until the next frame has begun. This would make it easier to sync your changes to the frame.

Actually, I guess for #2, a simpler and more general solution would be to just provide access to Time.frameCount as an intrinsic function. Then you could implement waitForNextFrame yourself as:

waitForNextFrame = function()
    curFrame = frameCount
    while frameCount == curFrame
    end while
end function

Will it be free solution or a paid package (if the latter, what price)? I’m looking for something different than Lua. Aactually wanted to use Common Lisp, but there are no implementation of Common Lisp that works with Unity, only Clojure and scheme, I think (clojure’s implementation’s seemingly stopped and whoever made scheme integration didn’t make it public). Darn, why they can’t make interpreter for most powerful language in the world available for Unity users?

It will be a paid solution, so I can support it better, but it won’t be too much — maybe something in the $50 range.

I wrote Lisp code all day for about a year when I worked in a neuroscience lab at the Salk Institute, and, well… the less said about that, the better. :slight_smile:

I’ve added a .remove(k) intrinsic that works on most types:

  • on a map, it looks for key k, and if it exists, removes it and returns 1; otherwise, returns 0.
  • on a list, it removes index k (allowing negative indexes to count from the end as usual), raising an error if k is out of bounds, and returns the mutated list.
  • on a string, it returns the string with the first occurrence of substring k removed (since strings are immutable).

Try it out in the updated demo.

I’m trying hard to balance two competing urges: keep MiniScript as small and simple as possible, and add any functionality that somebody would dope-slap me for leaving out.

So, anything the user could not do themselves, I definitely need to add. Removing elements was certainly in this category.

Anything the user could do themselves, but it would be horribly inefficient compared to doing it as an intrinsic, I should probably add unless it’s just not a common need.

Anything the user could do themselves without too much trouble, I should leave out.

But at the same time, because Miniscript is weakly typed (OK, fine, dynamically typed for all you Python fans), I’m inclined to make methods work on as many of the intrinsic types as possible. So, take contains for example… we don’t have a contains method yet, but we might, because that’s the sort of thing that’s hard to do yourself and commonly needed, at least on strings (check for substring), maps (check for key), and lists (check for element). But then I say to myself, what about numbers? You can think of any composite number as “containing” all its factors. So, rather than just throwing an error, should x.contains(y) do the same thing as x % y == 0?

But as soon as we do that, then we may as well make remove work for numbers too (since remove is sort of an inverse of contains). So x.remove(y) would do the same as x / y. And at this point, I feel I’m wandering into madness, pursued by foolishly consistent hobgoblins.

Besides, we normally try to treat lists and maps in the same way, except that lists only take a numeric index as the key. So I guess what we really want is indexOf on strings, lists, and maps (in the latter case doing a reverse look-up), and then perhaps a hasIndex method on maps (checks for key) and lists and strings (checks whether the argument would be a valid index).

If anybody wants to join me on my narrow escape from madness, please jump right in!

I’ve just updated MiniScript with these enhancements:

  • New indexOf intrinsic returns the index or key of the given value in a list, string, or map (or null if not found).
  • New hasIndex intrinsic returns 1 if the given index is within range for a list or string, or is a key found in the given map (returning 0 otherwise).
  • New null keyword evaluates to null (surprise!).

I also fixed a couple of bugs in the evaluation of “and” and “or” with mixed operand types, and indexing into strings (for example foo[-1] to get the last character of some string foo).

Some food for though: what’s the use case? If it’s just intended to be a general purpose, non-compiled scripting language, then it seems necessary to make it feature rich. If it actually had a narrow use case, then it would be easier to draw a line in the sand to say that something isn’t vital.
Another issue is if you intend to provide source or not is also going to impact whether the impetus is on you to provide the necessary features.

1 Like

Good points.

It’s intended to be used as an embedded scripting language in games, but I don’t know what the specific use case might be, as that will vary. Could be modding, could be a hardcore programming game, could be an educational game targeting yoots. There could even be somebody who wants to code most of their game logic in it, so they can update their game via Asset Bundles, without exposing this to the player at all.

So, yeah, I think this means I’m going to have to make it fairly feature-rich.

As for source: yes, I intended to provide source, and sure, that means developers can add their own functions. They’re pretty easy to do, too; for example, here’s the source for a hash function that returns a numeric hash for any value:

            // hash
            f = Intrinsic.Create("hash");
            f.AddParam("obj");
            f.code = (context, partialResult) => {
                Value val = context.GetVar("obj");
                return new Intrinsic.Result(val.Hash());
            };

But, I’ve seen cases where a language with modest ambitions happens to catch on (I think Lua is a case of this). If that should happen to MiniScript, and every developer has had to add their own string replace function or whatever, all slightly different, then the result is a mess for end-users. So it’s probably better to provide a big enough set of intrinsics that developers don’t feel they need to fill in basic gaps, and can instead just add whatever is unique to their game.

I have looked at this yet but it would be cool if it was more natural language like.

You mean something like AppleScript or COBOL?

Those sorts of languages tend to be easy to read but hard to write (sometimes known as read-only languages). Of course it’s better than something like Perl or assembly, which is fairly easy to write but often nearly impossible to read!

I’m hoping to have struck a good middle ground here… the syntax is considerably more natural than C, but still simple and regular enough that (I hope) it’s easy to both read and write.