-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
What's the right API for variables and lists? #19
Comments
ProposalLet's answer the questions in order: 1. Split variables and lists?Scratch makes a clear distinction between variables and lists. Javascript does not (arrays are stored in variables). Scratch-js it at the awkward intersection of these worlds, and we need to make a judgement call. One of the things that makes scratch-js exciting to me is that it's possible to get all the functionality of Scratch (with its nice-to-haves like automatic rendering) while learning to write javascript proper. Much like Snap!, I think it's important to make decisions that enforce good habits and teach common programming paradigms to Scratch users. In that light, I think it makes sense to push people towards an understanding that lists are a data type to be stored in variables. Let's merge variables and lists into one concept. 2. Split sprite and stage variables?Globally accessible (stage) variables are a lot different than private (sprite) variables no matter how you spin it. When writing code, the difference should be clear. (Weirdly, Scratch does a terrible job of this.) The only real downside to splitting up sprite vs. stage variables is that it makes compiling Scratch projects a bit harder. That's okay though; it's worth the sacrifice. 3. How to access variables?When doing object-oriented programming with javascript, one might choose to set values on an instance of a class. (
To solve the last problem we could store variables as a single object called The optionsOption 1: Pretty and terrifyingthis.myVariable = 5;
this.stage.otherVar = 3;
console.log(this.myVariable + this.stage.otherVar);
// Possible issue:
this.goto = 10; // Oh no! We just replaced the goto method with the number 10! Option 2: Ugly and safethis.vars.myVariable = 5;
this.stage.vars.otherVar = 3;
console.log(this.vars.myVariable + this.stage.vars.otherVar); Option 3?Is there something better possible? (Please leave recommendations!) We could always get fancy with getters, setters, and proxies to create just about any concoction we so desire. It's all about getting the design right; implementation is easy, even with crazy ideas. |
With option 1 it is possible to prevent overwriting built-in methods and variables (so that when you run |
After sleeping on it (and realizing that there's more complexity to this than I had originally considered), I think it makes sense to go with option 2 ( What I had failed to consider initially is variable watchers. We don't need to nail down the specifics on watchers right now, but they're definitely going to require a fairly standardized way to access a sprite's variables. Option 2 it is. Watchers conceptWhile I'm thinking about watchers, here's a quick concept for how they could work. // Initializing a sprite
new MySprite(
{
x: 0,
y: 0,
// ...
},
{
// Define sprite variables
myListName: [1, 2, 3]
}
)
// Initialize project with watchers
new Project(
stage,
sprites,
[
{
// Global "answer" value shown in top left corner
watch: [Stage, "sensing", "answer"],
x1: -230,
y1: 170
},
{
// x position of MySprite shown in top right corner
watch: [MySprite, "motion", "x"],
x2: 230,
y1: 170
},
{
// MySprite's variable "myListName" shown on most of screen
watch: [MySprite, "vars", "myListName"],
x1: -230,
x2: 230,
y1: 150,
y2: -170
}
]
) My thinking is that watchers should be able to show any value you throw at them. Whatever you store in a variable, whether it's a number, string, array, or object, it should show up nicely (while still resembling Scratch's watchers for basic types). |
The simplified variables API (option 2) has been implemented in dc27d61 |
Proxies let us do some pretty fancy stuff without resorting to // -- Imagine we setup some Proxy p with get()/set() traps --
let p
p.move = 'foo'
yield p.goto(10, 10) // Woah! We called the original method!
console.log(p.goto) // 'foo' Doing variables with proxies also means you can limit datatypes and do other magical things, like watchers: // Can do this *without* modifying String.prototype:
p.goto = 'foo'
p.goto.watch() // Displays watcher.
p.move = 'bar' Doing it like this may be evil, though; teaching the wrong ideas ( this.myVariable = 'foo'
this.goto = 0 // throws Error: cannot mutate Sprite method 'goto'
console.log(this.goto) // Function goto This isn't very JS-like, though, since Going a totally different route, something more akin to the following might be cute: // I have no idea how scratch-js works, sorry :shipit:
function * tick() {
// 'this' refers to sprite
this.myVariable = 0
yield moveSteps(10, 20) // note lack of this-- engine methods could be calls out to some global(ish) function which uses `this` to determine the sprite etc
} Or, just, um, use the var globalVariable = 'foo'
/* in sprite but no particular script */ {
var ourVariable = 'bar'
/* in script */ {
var myVariable = 'baz'
// ...
}
} This might require a rethink of how |
@nanaian There's a lot of fun to be had with proxies, but I don't think much of it is useful when the goal is to create a bog-standard API without any gimmicks (with the intent of teaching programming best practices for everyday situations). You've hit on an interesting idea at the end with your most recent edit. It's the kind of idea that's good because it's so obvious in hindsight. Sprite vs. stage variables map very cleanly to scope in JS, so it totally makes sense to use that as the basis of the variable system. You're also correct when you say that the scope-based approach doesn't really fit with the existing model. Currently, sprites (and the stage) are defined as classes that extend the built-in scratch-js base I really like using classes for sprites (and the stage). Game design is a great use case for OOP, and it's also a nice model for dealing with clones (just create more instances of the class!). Is there a way to get the best of both worlds? |
How about this? // Cat.mjs
import * from 'https://scratch.js.org/blocks.mjs'
let ourVariable = 'foo' // Shared between all Cats.
// Note that Cat does *not* extend any kind of Sprite class!
export default class Cat {
constructor(name) {
this.name = name // Local to this Cat instance only.
// Declarative, rather than `this.costumes = [...]` etc.
// Works by messing around with `this[SPRITE].costumes` (where `SPRITE` is a `Symbol`) etc.
addCostume('cat', './cat.svg', { x: 47, y: 55 })
onClick(this.clicked)
}
* clicked() {
say(`Meow! My name is ${this.name}`)
// Make a clone of this cat.
let child = clone(this)
child::goto(10, 10) // [1]
}
} [1] See https://github.com/tc39/proposal-bind-operator. Alternatively use // index.mjs
import { createSprite } from 'https://scratch.js.org/project.mjs'
import Cat from './Cat.mjs'
createSprite(Cat) Inheriting from |
Interesting, but this still doesn't allow sharing scope-based state between different sprites. As far as I can tell, sharing based on scope is essentially nonexistent when using imports. Plus, sacrificing actual instance methods is a big cost. (That being said, I appreciate the effort and am still very curious about this idea...) (Also, regarding |
For now, option 2 seems to be working okay. Going to close for now. |
Right now there's an API in place for replicating Scratch's variables, but it has some problems. The way it works right now is that scripts run by a trigger are passed two variables: one object containing the global variables, and another containing the sprite variables. Unfortunately, this setup means that variables are not currently accessible from within custom blocks (methods) unless they are passed manually. This is really bad.
We need a better system.
In Scratch, there are a variety of variable types:
(Per-script variables don't exist in Scratch like they do in Snap!, but they should in scratch-js and will exist essentially by default.)
Cloud variables rely on server infrastructure to operate, so they probably don't make sense to support in scratch-js, at least in the beginning.
This leaves us with variables and lists at the sprite and stage level. There are few options to consider:
The text was updated successfully, but these errors were encountered: