mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-07-12 14:03:57 -04:00
Clones (#150)
* Provide property to Clone to distinguish "original" clones * Provide method to clone a clone's properties * Don't report clones in the UI target list * Add target info to Thread * Allow hats to skip clones (for green flag) * Green flag skips clones * Implement "create clone" and hat * Pass the runtime to sprites and clones (for start hats) * Clone disposal; trigger hats after drawable initializes. * Separate stop threads for target; fix handling of stop button * Remove extraneous `skipClones` property * Add global clone limit * Don't allow a non-clone to delete itself. * Rename `cloneClone` -> `makeClone` * Variable updates in runtime.js * Synchronous drawable initialization (until we put it back to promises)
This commit is contained in:
parent
542899949e
commit
9744bcbb70
10 changed files with 229 additions and 37 deletions
src
|
@ -21,7 +21,18 @@ Scratch3ControlBlocks.prototype.getPrimitives = function() {
|
|||
'control_wait': this.wait,
|
||||
'control_if': this.if,
|
||||
'control_if_else': this.ifElse,
|
||||
'control_stop': this.stop
|
||||
'control_stop': this.stop,
|
||||
'control_create_clone_of_menu': this.createCloneMenu,
|
||||
'control_create_clone_of': this.createClone,
|
||||
'control_delete_this_clone': this.deleteClone
|
||||
};
|
||||
};
|
||||
|
||||
Scratch3ControlBlocks.prototype.getHats = function () {
|
||||
return {
|
||||
'control_start_as_clone': {
|
||||
restartExistingThreads: false
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -118,4 +129,30 @@ Scratch3ControlBlocks.prototype.stop = function() {
|
|||
this.runtime.stopAll();
|
||||
};
|
||||
|
||||
// @todo (GH-146): remove.
|
||||
Scratch3ControlBlocks.prototype.createCloneMenu = function (args) {
|
||||
return args.CLONE_OPTION;
|
||||
};
|
||||
|
||||
Scratch3ControlBlocks.prototype.createClone = function (args, util) {
|
||||
var cloneTarget;
|
||||
if (args.CLONE_OPTION == '_myself_') {
|
||||
cloneTarget = util.target;
|
||||
} else {
|
||||
cloneTarget = this.runtime.getSpriteTargetByName(args.CLONE_OPTION);
|
||||
}
|
||||
if (!cloneTarget) {
|
||||
return;
|
||||
}
|
||||
var newClone = cloneTarget.makeClone();
|
||||
if (newClone) {
|
||||
this.runtime.targets.push(newClone);
|
||||
}
|
||||
};
|
||||
|
||||
Scratch3ControlBlocks.prototype.deleteClone = function (args, util) {
|
||||
this.runtime.disposeTarget(util.target);
|
||||
this.runtime.stopForTarget(util.target);
|
||||
};
|
||||
|
||||
module.exports = Scratch3ControlBlocks;
|
||||
|
|
|
@ -16,7 +16,7 @@ var isPromise = function (value) {
|
|||
*/
|
||||
var execute = function (sequencer, thread) {
|
||||
var runtime = sequencer.runtime;
|
||||
var target = runtime.targetForThread(thread);
|
||||
var target = thread.target;
|
||||
|
||||
// Current block to execute is the one on the top of the stack.
|
||||
var currentBlockId = thread.peekStack();
|
||||
|
|
|
@ -60,6 +60,11 @@ function Runtime () {
|
|||
|
||||
this._scriptGlowsPreviousFrame = [];
|
||||
this._editingTarget = null;
|
||||
/**
|
||||
* Currently known number of clones.
|
||||
* @type {number}
|
||||
*/
|
||||
this._cloneCounter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -102,6 +107,11 @@ util.inherits(Runtime, EventEmitter);
|
|||
*/
|
||||
Runtime.THREAD_STEP_INTERVAL = 1000 / 60;
|
||||
|
||||
/**
|
||||
* How many clones can be created at a time.
|
||||
* @const {number}
|
||||
*/
|
||||
Runtime.MAX_CLONES = 300;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
|
@ -194,11 +204,13 @@ Runtime.prototype.clearEdgeActivatedValues = function () {
|
|||
|
||||
/**
|
||||
* Create a thread and push it to the list of threads.
|
||||
* @param {!string} id ID of block that starts the stack
|
||||
* @param {!string} id ID of block that starts the stack.
|
||||
* @param {!Target} target Target to run thread on.
|
||||
* @return {!Thread} The newly created thread.
|
||||
*/
|
||||
Runtime.prototype._pushThread = function (id) {
|
||||
Runtime.prototype._pushThread = function (id, target) {
|
||||
var thread = new Thread(id);
|
||||
thread.setTarget(target);
|
||||
thread.pushStack(id);
|
||||
this.threads.push(thread);
|
||||
return thread;
|
||||
|
@ -237,7 +249,7 @@ Runtime.prototype.toggleScript = function (topBlockId) {
|
|||
}
|
||||
}
|
||||
// Otherwise add it.
|
||||
this._pushThread(topBlockId);
|
||||
this._pushThread(topBlockId, this._editingTarget);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -306,7 +318,8 @@ Runtime.prototype.startHats = function (requestedHatOpcode,
|
|||
// If `restartExistingThreads` is true, we should stop
|
||||
// any existing threads starting with the top block.
|
||||
for (var i = 0; i < instance.threads.length; i++) {
|
||||
if (instance.threads[i].topBlock === topBlockId) {
|
||||
if (instance.threads[i].topBlock === topBlockId &&
|
||||
(!opt_target || instance.threads[i].target == opt_target)) {
|
||||
instance._removeThread(instance.threads[i]);
|
||||
}
|
||||
}
|
||||
|
@ -314,31 +327,72 @@ Runtime.prototype.startHats = function (requestedHatOpcode,
|
|||
// If `restartExistingThreads` is false, we should
|
||||
// give up if any threads with the top block are running.
|
||||
for (var j = 0; j < instance.threads.length; j++) {
|
||||
if (instance.threads[j].topBlock === topBlockId) {
|
||||
if (instance.threads[j].topBlock === topBlockId &&
|
||||
(!opt_target || instance.threads[j].target == opt_target)) {
|
||||
// Some thread is already running.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start the thread with this top block.
|
||||
newThreads.push(instance._pushThread(topBlockId));
|
||||
newThreads.push(instance._pushThread(topBlockId, target));
|
||||
}, opt_target);
|
||||
return newThreads;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispose of a target.
|
||||
* @param {!Target} target Target to dispose of.
|
||||
*/
|
||||
Runtime.prototype.disposeTarget = function (target) {
|
||||
// Allow target to do dispose actions.
|
||||
target.dispose();
|
||||
// Remove from list of targets.
|
||||
var index = this.targets.indexOf(target);
|
||||
if (index > -1) {
|
||||
this.targets.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop any threads acting on the target.
|
||||
* @param {!Target} target Target to stop threads for.
|
||||
*/
|
||||
Runtime.prototype.stopForTarget = function (target) {
|
||||
// Stop any threads on the target.
|
||||
for (var i = 0; i < this.threads.length; i++) {
|
||||
if (this.threads[i].target == target) {
|
||||
this._removeThread(this.threads[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Start all threads that start with the green flag.
|
||||
*/
|
||||
Runtime.prototype.greenFlag = function () {
|
||||
this.stopAll();
|
||||
this.ioDevices.clock.resetProjectTimer();
|
||||
this.clearEdgeActivatedValues();
|
||||
this.startHats('event_whenflagclicked');
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop "everything"
|
||||
* Stop "everything."
|
||||
*/
|
||||
Runtime.prototype.stopAll = function () {
|
||||
// Dispose all clones.
|
||||
var newTargets = [];
|
||||
for (var i = 0; i < this.targets.length; i++) {
|
||||
if (this.targets[i].hasOwnProperty('isOriginal') &&
|
||||
!this.targets[i].isOriginal) {
|
||||
this.targets[i].dispose();
|
||||
} else {
|
||||
newTargets.push(this.targets[i]);
|
||||
}
|
||||
}
|
||||
this.targets = newTargets;
|
||||
// Dispose all threads.
|
||||
var threadsCopy = this.threads.slice();
|
||||
while (threadsCopy.length > 0) {
|
||||
var poppedThread = threadsCopy.pop();
|
||||
|
@ -379,7 +433,7 @@ Runtime.prototype._updateScriptGlows = function () {
|
|||
// Find all scripts that should be glowing.
|
||||
for (var i = 0; i < this.threads.length; i++) {
|
||||
var thread = this.threads[i];
|
||||
var target = this.targetForThread(thread);
|
||||
var target = thread.target;
|
||||
if (thread.requestScriptGlowInFrame && target == this._editingTarget) {
|
||||
var blockForThread = thread.peekStack() || thread.topBlock;
|
||||
var script = target.blocks.getTopLevelScript(blockForThread);
|
||||
|
@ -458,23 +512,6 @@ Runtime.prototype.visualReport = function (blockId, value) {
|
|||
this.emit(Runtime.VISUAL_REPORT, blockId, String(value));
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the Target for a particular thread.
|
||||
* @param {!Thread} thread Thread to determine target for.
|
||||
* @return {?Target} Target object, if one exists.
|
||||
*/
|
||||
Runtime.prototype.targetForThread = function (thread) {
|
||||
// @todo This is a messy solution,
|
||||
// but prevents having circular data references.
|
||||
// Have a map or some other way to associate target with threads.
|
||||
for (var t = 0; t < this.targets.length; t++) {
|
||||
var target = this.targets[t];
|
||||
if (target.blocks.getBlock(thread.topBlock)) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a target by its id.
|
||||
* @param {string} targetId Id of target to find.
|
||||
|
@ -489,6 +526,36 @@ Runtime.prototype.getTargetById = function (targetId) {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the first original (non-clone-block-created) sprite given a name.
|
||||
* @param {string} spriteName Name of sprite to look for.
|
||||
* @return {?Target} Target representing a sprite of the given name.
|
||||
*/
|
||||
Runtime.prototype.getSpriteTargetByName = function (spriteName) {
|
||||
for (var i = 0; i < this.targets.length; i++) {
|
||||
var target = this.targets[i];
|
||||
if (target.sprite && target.sprite.name == spriteName) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the clone counter to track how many clones are created.
|
||||
* @param {number} changeAmount How many clones have been created/destroyed.
|
||||
*/
|
||||
Runtime.prototype.changeCloneCounter = function (changeAmount) {
|
||||
this._cloneCounter += changeAmount;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return whether there are clones available.
|
||||
* @return {boolean} True until the number of clones hits Runtime.MAX_CLONES.
|
||||
*/
|
||||
Runtime.prototype.clonesAvailable = function () {
|
||||
return this._cloneCounter < Runtime.MAX_CLONES;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a target representing the Scratch stage, if one exists.
|
||||
* @return {?Target} The target, if found.
|
||||
|
|
|
@ -111,7 +111,7 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) {
|
|||
branchNum = 1;
|
||||
}
|
||||
var currentBlockId = thread.peekStack();
|
||||
var branchId = this.runtime.targetForThread(thread).blocks.getBranch(
|
||||
var branchId = thread.target.blocks.getBranch(
|
||||
currentBlockId,
|
||||
branchNum
|
||||
);
|
||||
|
@ -155,8 +155,7 @@ Sequencer.prototype.proceedThread = function (thread) {
|
|||
// Pop from the stack - finished this level of execution.
|
||||
thread.popStack();
|
||||
// Push next connected block, if there is one.
|
||||
var nextBlockId = (this.runtime.targetForThread(thread).
|
||||
blocks.getNextBlock(currentBlockId));
|
||||
var nextBlockId = thread.target.blocks.getNextBlock(currentBlockId);
|
||||
if (nextBlockId) {
|
||||
thread.pushStack(nextBlockId);
|
||||
}
|
||||
|
|
|
@ -37,4 +37,12 @@ Target.prototype.getName = function () {
|
|||
return this.id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Call to destroy a target.
|
||||
* @abstract
|
||||
*/
|
||||
Target.prototype.dispose = function () {
|
||||
|
||||
};
|
||||
|
||||
module.exports = Target;
|
||||
|
|
|
@ -29,6 +29,12 @@ function Thread (firstBlock) {
|
|||
*/
|
||||
this.status = 0; /* Thread.STATUS_RUNNING */
|
||||
|
||||
/**
|
||||
* Target of this thread.
|
||||
* @type {?Target}
|
||||
*/
|
||||
this.target = null;
|
||||
|
||||
/**
|
||||
* Whether the thread requests its script to glow during this frame.
|
||||
* @type {boolean}
|
||||
|
@ -145,4 +151,20 @@ Thread.prototype.setStatus = function (status) {
|
|||
this.status = status;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set thread target.
|
||||
* @param {?Target} target Target for this thread.
|
||||
*/
|
||||
Thread.prototype.setTarget = function (target) {
|
||||
this.target = target;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get thread target.
|
||||
* @return {?Target} Target for this thread, if available.
|
||||
*/
|
||||
Thread.prototype.getTarget = function () {
|
||||
return this.target;
|
||||
};
|
||||
|
||||
module.exports = Thread;
|
||||
|
|
|
@ -40,7 +40,7 @@ function parseScratchObject (object, runtime, topLevel) {
|
|||
// Blocks container for this object.
|
||||
var blocks = new Blocks();
|
||||
// @todo: For now, load all Scratch objects (stage/sprites) as a Sprite.
|
||||
var sprite = new Sprite(blocks);
|
||||
var sprite = new Sprite(blocks, runtime);
|
||||
// Sprite/stage name from JSON.
|
||||
if (object.hasOwnProperty('objName')) {
|
||||
sprite.name = object.objName;
|
||||
|
|
|
@ -207,7 +207,10 @@ VirtualMachine.prototype.setEditingTarget = function (targetId) {
|
|||
VirtualMachine.prototype.emitTargetsUpdate = function () {
|
||||
this.emit('targetsUpdate', {
|
||||
// [[target id, human readable target name], ...].
|
||||
targetList: this.runtime.targets.map(function(target) {
|
||||
targetList: this.runtime.targets.filter(function (target) {
|
||||
// Don't report clones.
|
||||
return !target.hasOwnProperty('isOriginal') || target.isOriginal;
|
||||
}).map(function(target) {
|
||||
return [target.id, target.getName()];
|
||||
}),
|
||||
// Currently editing target id.
|
||||
|
|
|
@ -5,10 +5,12 @@ var Target = require('../engine/target');
|
|||
/**
|
||||
* Clone (instance) of a sprite.
|
||||
* @param {!Sprite} sprite Reference to the sprite.
|
||||
* @param {Runtime} runtime Reference to the runtime.
|
||||
* @constructor
|
||||
*/
|
||||
function Clone(sprite) {
|
||||
function Clone(sprite, runtime) {
|
||||
Target.call(this, sprite.blocks);
|
||||
this.runtime = runtime;
|
||||
/**
|
||||
* Reference to the sprite that this is a clone of.
|
||||
* @type {!Sprite}
|
||||
|
@ -29,8 +31,6 @@ function Clone(sprite) {
|
|||
* @type {?Number}
|
||||
*/
|
||||
this.drawableID = null;
|
||||
|
||||
this.initDrawable();
|
||||
}
|
||||
util.inherits(Clone, Target);
|
||||
|
||||
|
@ -42,9 +42,22 @@ Clone.prototype.initDrawable = function () {
|
|||
this.drawableID = this.renderer.createDrawable();
|
||||
this.updateAllDrawableProperties();
|
||||
}
|
||||
// If we're a clone, start the hats.
|
||||
if (!this.isOriginal) {
|
||||
this.runtime.startHats(
|
||||
'control_start_as_clone', null, this
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Clone-level properties.
|
||||
/**
|
||||
* Whether this represents an "original" clone, i.e., created by the editor
|
||||
* and not clone blocks. In interface terms, this true for a "sprite."
|
||||
* @type {boolean}
|
||||
*/
|
||||
Clone.prototype.isOriginal = true;
|
||||
|
||||
/**
|
||||
* Whether this clone represents the Scratch stage.
|
||||
* @type {boolean}
|
||||
|
@ -298,4 +311,40 @@ Clone.prototype.colorIsTouchingColor = function (targetRgb, maskRgb) {
|
|||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Make a clone of this clone, copying any run-time properties.
|
||||
* If we've hit the global clone limit, returns null.
|
||||
* @return {!Clone} New clone object.
|
||||
*/
|
||||
Clone.prototype.makeClone = function () {
|
||||
if (!this.runtime.clonesAvailable()) {
|
||||
return; // Hit max clone limit.
|
||||
}
|
||||
this.runtime.changeCloneCounter(1);
|
||||
var newClone = this.sprite.createClone();
|
||||
newClone.x = this.x;
|
||||
newClone.y = this.y;
|
||||
newClone.direction = this.direction;
|
||||
newClone.visible = this.visible;
|
||||
newClone.size = this.size;
|
||||
newClone.currentCostume = this.currentCostume;
|
||||
newClone.effects = JSON.parse(JSON.stringify(this.effects));
|
||||
newClone.initDrawable();
|
||||
newClone.updateAllDrawableProperties();
|
||||
return newClone;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispose of this clone, destroying any run-time properties.
|
||||
*/
|
||||
Clone.prototype.dispose = function () {
|
||||
if (this.isOriginal) { // Don't allow a non-clone to delete itself.
|
||||
return;
|
||||
}
|
||||
this.runtime.changeCloneCounter(-1);
|
||||
if (this.renderer && this.drawableID !== null) {
|
||||
this.renderer.destroyDrawable(this.drawableID);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Clone;
|
||||
|
|
|
@ -5,9 +5,11 @@ var Blocks = require('../engine/blocks');
|
|||
* Sprite to be used on the Scratch stage.
|
||||
* All clones of a sprite have shared blocks, shared costumes, shared variables.
|
||||
* @param {?Blocks} blocks Shared blocks object for all clones of sprite.
|
||||
* @param {Runtime} runtime Reference to the runtime.
|
||||
* @constructor
|
||||
*/
|
||||
function Sprite (blocks) {
|
||||
function Sprite (blocks, runtime) {
|
||||
this.runtime = runtime;
|
||||
if (!blocks) {
|
||||
// Shared set of blocks for all clones.
|
||||
blocks = new Blocks();
|
||||
|
@ -43,8 +45,13 @@ function Sprite (blocks) {
|
|||
* @returns {!Clone} Newly created clone.
|
||||
*/
|
||||
Sprite.prototype.createClone = function () {
|
||||
var newClone = new Clone(this);
|
||||
var newClone = new Clone(this, this.runtime);
|
||||
newClone.isOriginal = this.clones.length == 0;
|
||||
this.clones.push(newClone);
|
||||
if (newClone.isOriginal) {
|
||||
newClone.initDrawable();
|
||||
newClone.updateAllDrawableProperties();
|
||||
}
|
||||
return newClone;
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue