Skip to content

Commit 0b7abff

Browse files
authored
Merge pull request #324 from scratchcpp/ifonedgebounce_block
Implement "if on edge, bounce" block
2 parents 5fb6b8f + 9acdf41 commit 0b7abff

File tree

7 files changed

+385
-1
lines changed

7 files changed

+385
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ int main(int argc, char **argv) {
117117
<!-- ROADMAP -->
118118
## Roadmap
119119
120-
- [ ] Motion blocks
120+
- [x] Motion blocks
121121
- [ ] Looks blocks
122122
- [ ] Sound blocks
123123
- [ ] Event blocks

include/scratchcpp/sprite.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class LIBSCRATCHCPP_EXPORT Sprite : public Target
6565
void setRotationStyle(const char *newRotationStyle);
6666

6767
Rect boundingRect() const;
68+
void keepInFence(double newX, double newY, double *fencedX, double *fencedY) const;
6869

6970
double graphicsEffectValue(IGraphicsEffect *effect) const;
7071
void setGraphicsEffectValue(IGraphicsEffect *effect, double value);

src/blocks/motionblocks.cpp

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <scratchcpp/sprite.h>
66
#include <scratchcpp/input.h>
77
#include <scratchcpp/field.h>
8+
#include <scratchcpp/rect.h>
89

910
#include "motionblocks.h"
1011
#include "../engine/internal/randomgenerator.h"
@@ -38,6 +39,7 @@ void MotionBlocks::registerBlocks(IEngine *engine)
3839
engine->addCompileFunction(this, "motion_setx", &compileSetX);
3940
engine->addCompileFunction(this, "motion_changeyby", &compileChangeYBy);
4041
engine->addCompileFunction(this, "motion_sety", &compileSetY);
42+
engine->addCompileFunction(this, "motion_ifonedgebounce", &compileIfOnEdgeBounce);
4143
engine->addCompileFunction(this, "motion_setrotationstyle", &compileSetRotationStyle);
4244
engine->addCompileFunction(this, "motion_xposition", &compileXPosition);
4345
engine->addCompileFunction(this, "motion_yposition", &compileYPosition);
@@ -196,6 +198,11 @@ void MotionBlocks::compileSetY(Compiler *compiler)
196198
compiler->addFunctionCall(&setY);
197199
}
198200

201+
void MotionBlocks::compileIfOnEdgeBounce(Compiler *compiler)
202+
{
203+
compiler->addFunctionCall(&ifOnEdgeBounce);
204+
}
205+
199206
void MotionBlocks::compileSetRotationStyle(Compiler *compiler)
200207
{
201208
int option = compiler->field(STYLE)->specialValueId();
@@ -628,6 +635,99 @@ unsigned int MotionBlocks::setY(VirtualMachine *vm)
628635
return 1;
629636
}
630637

638+
unsigned int MotionBlocks::ifOnEdgeBounce(VirtualMachine *vm)
639+
{
640+
// See https://github.com/scratchfoundation/scratch-vm/blob/c37745e97e6d8a77ad1dc31a943ea728dd17ba78/src/blocks/scratch3_motion.js#L186-L240
641+
Sprite *sprite = dynamic_cast<Sprite *>(vm->target());
642+
IEngine *engine = vm->engine();
643+
644+
if (!sprite || !engine)
645+
return 0;
646+
647+
Rect bounds = sprite->boundingRect();
648+
649+
// Measure distance to edges
650+
// Values are zero when the sprite is beyond
651+
unsigned int stageWidth = engine->stageWidth();
652+
unsigned int stageHeight = engine->stageHeight();
653+
double distLeft = std::max(0.0, (stageWidth / 2.0) + bounds.left());
654+
double distTop = std::max(0.0, (stageHeight / 2.0) - bounds.top());
655+
double distRight = std::max(0.0, (stageWidth / 2.0) - bounds.right());
656+
double distBottom = std::max(0.0, (stageHeight / 2.0) + bounds.bottom());
657+
658+
// Find the nearest edge
659+
// 1 - left
660+
// 2 - top
661+
// 3 - right
662+
// 4 - bottom
663+
unsigned short nearestEdge = 0;
664+
double minDist = std::numeric_limits<double>::infinity();
665+
666+
if (distLeft < minDist) {
667+
minDist = distLeft;
668+
nearestEdge = 1;
669+
}
670+
671+
if (distTop < minDist) {
672+
minDist = distTop;
673+
nearestEdge = 2;
674+
}
675+
676+
if (distRight < minDist) {
677+
minDist = distRight;
678+
nearestEdge = 3;
679+
}
680+
681+
if (distBottom < minDist) {
682+
minDist = distBottom;
683+
nearestEdge = 4;
684+
}
685+
686+
if (minDist > 0) {
687+
return 0; // Not touching any edge
688+
}
689+
690+
assert(nearestEdge != 0);
691+
692+
// Point away from the nearest edge
693+
double radians = (90 - sprite->direction()) * pi / 180;
694+
double dx = std::cos(radians);
695+
double dy = -std::sin(radians);
696+
697+
switch (nearestEdge) {
698+
case 1:
699+
// Left
700+
dx = std::max(0.2, std::abs(dx));
701+
break;
702+
703+
case 2:
704+
// Top
705+
dy = std::max(0.2, std::abs(dy));
706+
break;
707+
708+
case 3:
709+
// Right
710+
dx = 0 - std::max(0.2, std::abs(dx));
711+
break;
712+
713+
case 4:
714+
// Bottom
715+
dy = 0 - std::max(0.2, std::abs(dy));
716+
break;
717+
}
718+
719+
double newDirection = (180 / pi) * (std::atan2(dy, dx)) + 90;
720+
sprite->setDirection(newDirection);
721+
722+
// Keep within the stage
723+
double fencedX, fencedY;
724+
sprite->keepInFence(sprite->x(), sprite->y(), &fencedX, &fencedY);
725+
sprite->setX(fencedX);
726+
sprite->setY(fencedY);
727+
728+
return 0;
729+
}
730+
631731
unsigned int MotionBlocks::setLeftRightRotationStyle(VirtualMachine *vm)
632732
{
633733
Sprite *sprite = dynamic_cast<Sprite *>(vm->target());

src/blocks/motionblocks.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class MotionBlocks : public IBlockSection
6161
static void compileSetX(Compiler *compiler);
6262
static void compileChangeYBy(Compiler *compiler);
6363
static void compileSetY(Compiler *compiler);
64+
static void compileIfOnEdgeBounce(Compiler *compiler);
6465
static void compileSetRotationStyle(Compiler *compiler);
6566
static void compileXPosition(Compiler *compiler);
6667
static void compileYPosition(Compiler *compiler);
@@ -98,9 +99,13 @@ class MotionBlocks : public IBlockSection
9899
static unsigned int setX(VirtualMachine *vm);
99100
static unsigned int changeYBy(VirtualMachine *vm);
100101
static unsigned int setY(VirtualMachine *vm);
102+
103+
static unsigned int ifOnEdgeBounce(VirtualMachine *vm);
104+
101105
static unsigned int setLeftRightRotationStyle(VirtualMachine *vm);
102106
static unsigned int setDoNotRotateRotationStyle(VirtualMachine *vm);
103107
static unsigned int setAllAroundRotationStyle(VirtualMachine *vm);
108+
104109
static unsigned int xPosition(VirtualMachine *vm);
105110
static unsigned int yPosition(VirtualMachine *vm);
106111
static unsigned int direction(VirtualMachine *vm);

src/scratch/sprite.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,54 @@ Rect Sprite::boundingRect() const
353353
return ret;
354354
}
355355

356+
/*!
357+
* Keeps the desired position within the stage.
358+
* \param[in] New desired X position.
359+
* \param[in] New desired Y position.
360+
* \param[out] Fenced X position.
361+
* \param[out] Fenced Y position.
362+
*/
363+
void Sprite::keepInFence(double newX, double newY, double *fencedX, double *fencedY) const
364+
{
365+
// See https://github.com/scratchfoundation/scratch-vm/blob/05dcbc176f51da34aeb9165559fc6acba8087ff8/src/sprites/rendered-target.js#L915-L948
366+
IEngine *eng = engine();
367+
368+
if (!(fencedX && fencedY && eng))
369+
return;
370+
371+
double stageWidth = eng->stageWidth();
372+
double stageHeight = eng->stageHeight();
373+
Rect fence(-stageWidth / 2, stageHeight / 2, stageWidth / 2, -stageHeight / 2);
374+
Rect bounds;
375+
impl->getBoundingRect(&bounds);
376+
377+
// Adjust the known bounds to the target position
378+
bounds.setLeft(bounds.left() + newX - impl->x);
379+
bounds.setRight(bounds.right() + newX - impl->x);
380+
bounds.setTop(bounds.top() + newY - impl->y);
381+
bounds.setBottom(bounds.bottom() + newY - impl->y);
382+
383+
// Find how far we need to move the target position
384+
double dx = 0;
385+
double dy = 0;
386+
387+
if (bounds.left() < fence.left()) {
388+
dx += fence.left() - bounds.left();
389+
}
390+
if (bounds.right() > fence.right()) {
391+
dx += fence.right() - bounds.right();
392+
}
393+
if (bounds.top() > fence.top()) {
394+
dy += fence.top() - bounds.top();
395+
}
396+
if (bounds.bottom() < fence.bottom()) {
397+
dy += fence.bottom() - bounds.bottom();
398+
}
399+
400+
*fencedX = newX + dx;
401+
*fencedY = newY + dy;
402+
}
403+
356404
/*! Returns the value of the given graphics effect. */
357405
double Sprite::graphicsEffectValue(IGraphicsEffect *effect) const
358406
{

test/blocks/motion_blocks_test.cpp

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
#include <scratchcpp/input.h>
44
#include <scratchcpp/field.h>
55
#include <scratchcpp/sprite.h>
6+
#include <scratchcpp/costume.h>
7+
#include <scratchcpp/scratchconfiguration.h>
68
#include <enginemock.h>
79
#include <randomgeneratormock.h>
810
#include <clockmock.h>
11+
#include <imageformatfactorymock.h>
12+
#include <imageformatmock.h>
913

1014
#include "../common.h"
1115
#include "blocks/motionblocks.h"
@@ -115,6 +119,7 @@ TEST_F(MotionBlocksTest, RegisterBlocks)
115119
EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "motion_setx", &MotionBlocks::compileSetX));
116120
EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "motion_changeyby", &MotionBlocks::compileChangeYBy));
117121
EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "motion_sety", &MotionBlocks::compileSetY));
122+
EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "motion_ifonedgebounce", &MotionBlocks::compileIfOnEdgeBounce));
118123
EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "motion_setrotationstyle", &MotionBlocks::compileSetRotationStyle));
119124
EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "motion_xposition", &MotionBlocks::compileXPosition));
120125
EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "motion_yposition", &MotionBlocks::compileYPosition));
@@ -1085,6 +1090,142 @@ TEST_F(MotionBlocksTest, SetYImpl)
10851090
ASSERT_EQ(sprite.y(), 189.42);
10861091
}
10871092

1093+
TEST_F(MotionBlocksTest, IfOnEdgeBounce)
1094+
{
1095+
Compiler compiler(&m_engineMock);
1096+
1097+
auto block = std::make_shared<Block>("a", "motion_ifonedgebounce");
1098+
1099+
EXPECT_CALL(m_engineMock, functionIndex(&MotionBlocks::ifOnEdgeBounce)).WillOnce(Return(0));
1100+
1101+
compiler.init();
1102+
compiler.setBlock(block);
1103+
MotionBlocks::compileIfOnEdgeBounce(&compiler);
1104+
compiler.end();
1105+
1106+
ASSERT_EQ(compiler.bytecode(), std::vector<unsigned int>({ vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT }));
1107+
ASSERT_TRUE(compiler.constValues().empty());
1108+
}
1109+
1110+
TEST_F(MotionBlocksTest, IfOnEdgeBounceImpl)
1111+
{
1112+
static unsigned int bytecode[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT };
1113+
static BlockFunc functions[] = { &MotionBlocks::ifOnEdgeBounce };
1114+
1115+
auto imageFormatFactory = std::make_shared<ImageFormatFactoryMock>();
1116+
auto imageFormat = std::make_shared<ImageFormatMock>();
1117+
1118+
ScratchConfiguration::registerImageFormat("test", imageFormatFactory);
1119+
EXPECT_CALL(*imageFormatFactory, createInstance()).WillOnce(Return(imageFormat));
1120+
EXPECT_CALL(*imageFormat, width()).WillOnce(Return(0));
1121+
EXPECT_CALL(*imageFormat, height()).WillOnce(Return(0));
1122+
auto costume = std::make_shared<Costume>("costume1", "a", "test");
1123+
1124+
Sprite sprite;
1125+
sprite.addCostume(costume);
1126+
sprite.setCostumeIndex(0);
1127+
1128+
static char data[5] = "abcd";
1129+
EXPECT_CALL(*imageFormat, setData(5, data));
1130+
EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
1131+
EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
1132+
1133+
EXPECT_CALL(*imageFormat, colorAt(0, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
1134+
EXPECT_CALL(*imageFormat, colorAt(1, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
1135+
EXPECT_CALL(*imageFormat, colorAt(2, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 255)));
1136+
EXPECT_CALL(*imageFormat, colorAt(3, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
1137+
1138+
EXPECT_CALL(*imageFormat, colorAt(0, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
1139+
EXPECT_CALL(*imageFormat, colorAt(1, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 255)));
1140+
EXPECT_CALL(*imageFormat, colorAt(2, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
1141+
EXPECT_CALL(*imageFormat, colorAt(3, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 255)));
1142+
1143+
EXPECT_CALL(*imageFormat, colorAt(0, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 255)));
1144+
EXPECT_CALL(*imageFormat, colorAt(1, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
1145+
EXPECT_CALL(*imageFormat, colorAt(2, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
1146+
EXPECT_CALL(*imageFormat, colorAt(3, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
1147+
costume->setData(5, data);
1148+
1149+
sprite.setEngine(&m_engineMock);
1150+
1151+
VirtualMachine vm(&sprite, &m_engineMock, nullptr);
1152+
vm.setBytecode(bytecode);
1153+
vm.setFunctions(functions);
1154+
1155+
EXPECT_CALL(*imageFormat, width()).Times(9).WillRepeatedly(Return(4));
1156+
EXPECT_CALL(*imageFormat, height()).Times(9).WillRepeatedly(Return(3));
1157+
EXPECT_CALL(m_engineMock, stageWidth()).Times(9).WillRepeatedly(Return(480));
1158+
EXPECT_CALL(m_engineMock, stageHeight()).Times(9).WillRepeatedly(Return(360));
1159+
1160+
// No edge
1161+
EXPECT_CALL(m_engineMock, requestRedraw()).Times(3);
1162+
EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(2).WillRepeatedly(Return(false));
1163+
sprite.setX(100);
1164+
sprite.setY(60);
1165+
sprite.setDirection(-45);
1166+
vm.run();
1167+
1168+
ASSERT_EQ(vm.registerCount(), 0);
1169+
ASSERT_EQ(sprite.x(), 100);
1170+
ASSERT_EQ(sprite.y(), 60);
1171+
ASSERT_EQ(sprite.direction(), -45);
1172+
1173+
// Left edge
1174+
EXPECT_CALL(m_engineMock, requestRedraw()).Times(5);
1175+
EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(4).WillRepeatedly(Return(false));
1176+
sprite.setX(-240);
1177+
sprite.setY(60);
1178+
vm.reset();
1179+
vm.run();
1180+
1181+
ASSERT_EQ(vm.registerCount(), 0);
1182+
ASSERT_EQ(std::round(sprite.x() * 100) / 100, -238.23);
1183+
ASSERT_EQ(sprite.y(), 60);
1184+
ASSERT_EQ(std::round(sprite.direction() * 100) / 100, 45);
1185+
1186+
// Top edge
1187+
EXPECT_CALL(m_engineMock, requestRedraw()).Times(6);
1188+
EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(4).WillRepeatedly(Return(false));
1189+
sprite.setX(100);
1190+
sprite.setY(180);
1191+
sprite.setDirection(45);
1192+
vm.reset();
1193+
vm.run();
1194+
1195+
ASSERT_EQ(vm.registerCount(), 0);
1196+
ASSERT_EQ(sprite.x(), 100);
1197+
ASSERT_EQ(std::round(sprite.y() * 100) / 100, 178.23);
1198+
ASSERT_EQ(sprite.direction(), 135);
1199+
1200+
// Right edge
1201+
EXPECT_CALL(m_engineMock, requestRedraw()).Times(5);
1202+
EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(4).WillRepeatedly(Return(false));
1203+
sprite.setX(240);
1204+
sprite.setY(60);
1205+
vm.reset();
1206+
vm.run();
1207+
1208+
ASSERT_EQ(vm.registerCount(), 0);
1209+
ASSERT_EQ(std::round(sprite.x() * 100) / 100, 238.23);
1210+
ASSERT_EQ(sprite.y(), 60);
1211+
ASSERT_EQ(sprite.direction(), -135);
1212+
1213+
// Bottom edge
1214+
EXPECT_CALL(m_engineMock, requestRedraw()).Times(5);
1215+
EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(4).WillRepeatedly(Return(false));
1216+
sprite.setX(-100);
1217+
sprite.setY(-180);
1218+
vm.reset();
1219+
vm.run();
1220+
1221+
ASSERT_EQ(vm.registerCount(), 0);
1222+
ASSERT_EQ(sprite.x(), -100);
1223+
ASSERT_EQ(std::round(sprite.y() * 100) / 100, -178.23);
1224+
ASSERT_EQ(std::round(sprite.direction() * 100) / 100, -45);
1225+
1226+
ScratchConfiguration::removeImageFormat("test");
1227+
}
1228+
10881229
TEST_F(MotionBlocksTest, SetRotationStyle)
10891230
{
10901231
Compiler compiler(&m_engineMock);

0 commit comments

Comments
 (0)