Skip to content
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

Add Grid-Aligned behavior to Playlab #17009

Merged
merged 8 commits into from Aug 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
57 changes: 41 additions & 16 deletions apps/src/studio/Item.js
Expand Up @@ -125,18 +125,23 @@ export default class Item extends Collidable {
// Draw the item's current location.
Studio.drawDebugRect("itemCenter", this.x, this.y, 3, 3);

// In this stationary activity case, we don't need to do any of this
// update logic (facing the actor is handled every frame in display())
if (this.activity === constants.BEHAVIOR_WATCH_ACTOR) {
if (
this.activity === constants.BEHAVIOR_WATCH_ACTOR ||
this.activity === constants.BEHAVIOR_GRID_ALIGNED
) {
// In this stationary activity case, we don't need to do any of this
// update logic (facing the actor is handled every frame in display())
return;
}
if (this.activity === constants.BEHAVIOR_STOP) {
} else if (this.activity === constants.BEHAVIOR_STOP) {
// In this stationary activity case, we override the actor's facing and
// movement to force a "stop"
this.setDirection(Direction.NONE);

this.destGridX = undefined;
this.destGridY = undefined;
return;
}

if (!this.visible) {
return;
}
Expand Down Expand Up @@ -327,7 +332,8 @@ export default class Item extends Collidable {

/**
* Sets the activity property for this item.
* @param {string} type Valid options are: none, watchActor, roam, chase, or flee
* @param {string} type Valid options are: none, watchActor, roam, chase,
* flee, or grid
* @param {number} targetSpriteIndex optional target sprite used with chase and flee
*/
setActivity(type, targetSpriteIndex) {
Expand Down Expand Up @@ -387,6 +393,17 @@ export default class Item extends Collidable {
return this.startFadeTime && currentTime > this.startFadeTime + this.fadeTime;
}

/**
* Whether or not this sprite will turn to face south after
* Studio.ticksBeforeFaceSouth ticks of no movement
*
* @returns {boolean}
* @see Studio.onTick
*/
shouldFaceSouthOnIdle() {
return this.activity !== constants.BEHAVIOR_GRID_ALIGNED;
}

/**
* Display our item at its current location
*/
Expand Down Expand Up @@ -419,17 +436,25 @@ export default class Item extends Collidable {
};
}

/**
* Whether or not this sprite should automatically move on each tick.
*
* @returns {boolean}
* @see Studio.onTick
*/
shouldMove() {
const standstillBehaviors = [
constants.BEHAVIOR_STOP,
constants.BEHAVIOR_WATCH_ACTOR,
constants.BEHAVIOR_GRID_ALIGNED
];
return !standstillBehaviors.includes(this.activity);
}

getNextPosition() {
var unit = Direction.getUnitVector(this.dir);
var speed = this.speed;
// TODO: Better concept of which actions actually move the actor
// Projected position should not be in front of you if you are not moving!
if (
this.activity === constants.BEHAVIOR_STOP ||
this.activity === constants.BEHAVIOR_WATCH_ACTOR
) {
speed = 0;
}
const unit = Direction.getUnitVector(this.dir);
const speed = this.shouldMove() ? this.speed : 0;

return {
x: this.x + speed * unit.x,
y: this.y + speed * unit.y
Expand Down
4 changes: 3 additions & 1 deletion apps/src/studio/collidable.js
Expand Up @@ -6,6 +6,8 @@
*/
import { singleton as studioApp } from '../StudioApp';

import { BEHAVIOR_STOP } from '../constants';

export default class Collidable {
/**
* Collidable constructor opts
Expand All @@ -24,7 +26,7 @@ export default class Collidable {
this.gridX = undefined;
this.gridY = undefined;

this.activity = "none";
this.activity = BEHAVIOR_STOP;

for (var prop in opts) {
this[prop] = opts[prop];
Expand Down
1 change: 1 addition & 0 deletions apps/src/studio/constants.js
Expand Up @@ -418,6 +418,7 @@ export const BEHAVIOR_FLEE = 'flee';
export const BEHAVIOR_STOP = 'none';
export const BEHAVIOR_WANDER = 'roam';
export const BEHAVIOR_WATCH_ACTOR = 'watchActor';
export const BEHAVIOR_GRID_ALIGNED = 'grid';

// Take the screenshot almost immediately, hopefully catching the
// title screen and any characters in their initial positions.
Expand Down
27 changes: 27 additions & 0 deletions apps/src/studio/spriteActions.js
Expand Up @@ -37,6 +37,33 @@ import { valueOr } from '../utils';
* @returns {boolean} whether the action is finished running.
*/

/**
* Turn sprite toward a desired direction. Note that although this action can
* take place over the course of several steps/ticks, we only set the direction
* on the first tick; the sprite's own animation logic will take care of the
* rest.
* @constructor
* @implements {SpriteAction}
* @param {number} towardDir
* @param {number} totalSteps
*/
export class GridTurn {
constructor(towardDir, totalSteps) {
this.towardDir_ = towardDir;
this.totalSteps_ = totalSteps;
this.elapsedSteps_ = 0;
}
update(sprite) {
if (this.elapsedSteps_ === 0) {
sprite.setDirection(this.towardDir_);
}
this.elapsedSteps_++;
}
isDone() {
return this.elapsedSteps_ >= this.totalSteps_;
}
}

/**
* Move sprite by a desired delta over a certain number of steps/ticks.
* Used to provide discrete grid movement in playlab's continuous interpreted
Expand Down
100 changes: 74 additions & 26 deletions apps/src/studio/studio.js
Expand Up @@ -39,7 +39,7 @@ import dropletConfig from './dropletConfig';
import paramLists from './paramLists.js';
import studioCell from './cell';
import studioMsg from './locale';
import { GridMove, GridMoveAndCancel } from './spriteActions';
import { GridTurn, GridMove, GridMoveAndCancel } from './spriteActions';
import { Provider } from 'react-redux';
import { singleton as studioApp } from '../StudioApp';
import {
Expand Down Expand Up @@ -1305,16 +1305,20 @@ Studio.onTick = function () {
performQueuedMoves(i);
}

const sprite = Studio.sprite[i];

// After 5 ticks of no movement, turn sprite forward.
if (Studio.tickCount - Studio.sprite[i].lastMove > Studio.ticksBeforeFaceSouth) {
Studio.sprite[i].setDirection(Direction.NONE);
if (
sprite.shouldFaceSouthOnIdle() &&
(Studio.tickCount - sprite.lastMove > Studio.ticksBeforeFaceSouth)
) {
sprite.setDirection(Direction.NONE);
Studio.movementAudioOff();
}

// Display sprite:
Studio.displaySprite(i);

var sprite = Studio.sprite[i];
if (sprite.hasActions()) {
spritesNeedMoreAnimationFrames = true;
}
Expand Down Expand Up @@ -4259,29 +4263,29 @@ Studio.callCmd = function (cmd) {
case 'moveRight':
studioApp().highlight(cmd.id);
Studio.moveSingle({
spriteIndex: Studio.protagonistSpriteIndex || 0,
dir: Direction.EAST,
spriteIndex: Studio.protagonistSpriteIndex || 0,
dir: Direction.EAST,
});
break;
case 'moveLeft':
studioApp().highlight(cmd.id);
Studio.moveSingle({
spriteIndex: Studio.protagonistSpriteIndex || 0,
dir: Direction.WEST,
spriteIndex: Studio.protagonistSpriteIndex || 0,
dir: Direction.WEST,
});
break;
case 'moveUp':
studioApp().highlight(cmd.id);
Studio.moveSingle({
spriteIndex: Studio.protagonistSpriteIndex || 0,
dir: Direction.NORTH,
spriteIndex: Studio.protagonistSpriteIndex || 0,
dir: Direction.NORTH,
});
break;
case 'moveDown':
studioApp().highlight(cmd.id);
Studio.moveSingle({
spriteIndex: Studio.protagonistSpriteIndex || 0,
dir: Direction.SOUTH,
spriteIndex: Studio.protagonistSpriteIndex || 0,
dir: Direction.SOUTH,
});
break;
case 'moveForward':
Expand All @@ -4301,11 +4305,17 @@ Studio.callCmd = function (cmd) {
break;
case 'turnRight':
studioApp().highlight(cmd.id);
Studio.lastMoveSingleDir = turnRight90(Studio.lastMoveSingleDir);
Studio.turnSingle({
spriteIndex: Studio.protagonistSpriteIndex || 0,
dir: turnRight90(Studio.lastMoveSingleDir),
});
break;
case 'turnLeft':
studioApp().highlight(cmd.id);
Studio.lastMoveSingleDir = turnLeft90(Studio.lastMoveSingleDir);
Studio.turnSingle({
spriteIndex: Studio.protagonistSpriteIndex || 0,
dir: turnLeft90(Studio.lastMoveSingleDir),
});
break;
case 'moveDistance':
if (!cmd.opts.started) {
Expand Down Expand Up @@ -5510,7 +5520,7 @@ var createSpeechBubble = function (spriteIndex, text) {
Studio.stop = function (opts) {
cancelQueuedMovements(opts.spriteIndex, true);
cancelQueuedMovements(opts.spriteIndex, false);
Studio.sprite[opts.spriteIndex].activity = constants.BEHAVIOR_STOP;
Studio.sprite[opts.spriteIndex].setActivity(constants.BEHAVIOR_STOP);

if (!opts.dontResetCollisions) {
// Reset collisionMasks so the next movement will fire another collision
Expand Down Expand Up @@ -5857,6 +5867,53 @@ Studio.getSkin = function () {
return skin;
};

/**
* For grid-aligned movement, we want a single movement action to take place
* over several ticks (as opposed to normal movement, which takes place on a
* per-tick basis). We therefore yield control for the movement duration.
*
* @see Studio.turnSingle
* @see Studio.moveSingle
*/
Studio.yieldGridAlignedTicks = function () {
Studio.yieldExecutionTicks += (1 + Studio.gridAlignedExtraPauseSteps);
if (Studio.JSInterpreter) {
// Stop executing the interpreter in a tight loop and yield the current
// execution tick:
Studio.JSInterpreter.yield();
// Highlight the code in the editor so the student can see the progress
// of their program:
Studio.JSInterpreter.selectCurrentCode();
}

Studio.movementAudioOn();
};

/**
* For executing a single "goLeft" or "goNorth" sort of command in student code.
* Moves the avatar by a different amount.
* Has slightly different behaviors depending on whether the level is configured
* for discrete, grid-based movement or free movement.
* @param {Object} opts
* @param {Direction} opts.dir - The direction in which the sprite should move.
* @param {number} opts.spriteIndex
* @param {boolean} opts.backward - whether the sprite should move toward
* (default) or away from the given direction
*/
Studio.turnSingle = function (opts) {
if (!skin.gridAlignedMovement) {
throw new TypeError("Studio.turnSingle is only valid in grid-aligned mode");
}

const sprite = Studio.sprite[opts.spriteIndex];
sprite.lastMove = Studio.tickCount;
sprite.setActivity(constants.BEHAVIOR_GRID_ALIGNED);
sprite.addAction(new GridTurn(opts.dir, skin.slowExecutionFactor));

Studio.yieldGridAlignedTicks();
Studio.lastMoveSingleDir = opts.dir;
};

/**
* For executing a single "goLeft" or "goNorth" sort of command in student code.
* Moves the avatar by a different amount.
Expand Down Expand Up @@ -5916,6 +5973,7 @@ Studio.moveSingle = function (opts) {
}

if (skin.gridAlignedMovement) {
sprite.setActivity(constants.BEHAVIOR_GRID_ALIGNED);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to solicit some opinions on whether or not this is the correct place to set this behavior

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems good to me. It's actually weirder that I don't see a corresponding setActivity call in the else case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's the case that the else case expects BEHAVIOR_STOP, which is the default. If I'm correct then I could add that corresponding call for cleanliness, but if I'm not I have no idea what all could break

if (wallCollision || playspaceEdgeCollision) {
sprite.addAction(new GridMoveAndCancel(
deltaX, deltaY, skin.slowExecutionFactor, opts.backward));
Expand All @@ -5924,17 +5982,7 @@ Studio.moveSingle = function (opts) {
deltaX, deltaY, skin.slowExecutionFactor, opts.backward));
}

Studio.yieldExecutionTicks += (1 + Studio.gridAlignedExtraPauseSteps);
if (Studio.JSInterpreter) {
// Stop executing the interpreter in a tight loop and yield the current
// execution tick:
Studio.JSInterpreter.yield();
// Highlight the code in the editor so the student can see the progress
// of their program:
Studio.JSInterpreter.selectCurrentCode();
}

Studio.movementAudioOn();
Studio.yieldGridAlignedTicks();
} else {
if (!wallCollision) {
if (playspaceEdgeCollision) {
Expand Down