Advanced Topics: For Each Loops
||arrays:Arrays||
can be traversed using a simple ||loops:for||
loop,
using each index from 0 to one less than the ||arrays:length||
of
the ||arrays:array||
.
This can have a few downsides; for one, the index will often not be relevant
outside of keeping track of the different elements of the ||arrays:array||
.
Additionally, the ||arrays:array||
might be created and intended
to be used only within the loop,
like destroying random ||sprites:Enemies||
using ||sprites:sprites.allOfKind||
;
keeping the array around after the loop might result in bugs when
the array is unintentionally used later on.
||loops:For each||
loops will iterate through each element in an ||arrays:array||
.
Concept: For Each loop syntax
In JavaScript, a ||loops:for each||
loop is generally represented
using the ||loops:for ... of||
statement, shown in the snippet below.
let words: string[] = ["hello", "world"];
for (let element of words) {
game.splash(element);
}
In this snippet, ||variables:element||
is the loop variable
(like ||variables:i||
in a normal ||loops:for||
loop),
and is changed to a different element from the array ||variables:words||
on each iteration of the ||loops:loop||
.
This is done in order from the first element (index 0) to the last
element in the array.
Example #1: ||loops:For Each||
||sprites:Enemy||
- Review the code below
- Identify how the
||loops:for ... of||
loop is used to iterate over
all ||sprites:Enemy||
s
- Identify what is done to each
||sprites:Enemy||
in the ||loops:for ... of||
loop
for (let i = 0; i < 15; i++) {
let skeleton: Sprite = sprites.create(sprites.castle.skellyFront, SpriteKind.Enemy);
skeleton.x = randint(0, screen.width);
skeleton.y = randint(0, screen.height);
}
controller.A.onEvent(ControllerButtonEvent.Pressed, function () {
for (let enemy of sprites.allOfKind(SpriteKind.Enemy)) {
enemy.say("hi!", 1000);
}
});
Student Task #1: Spooky Skeleton
- Start with the code from example #1
- In the
||loops:for ... of||
loop, move each ||sprites:enemy||
up two pixels
- After the
||loops:for ... of||
loop, ||loops:pause||
for 1000 ms
- After the
||loops:pause||
, create another ||loops:for ... of||
over all ||sprites:sprites||
of ||sprites:kind||
||sprites:Enemy||
- In the second
||loops:for ... of||
loop, move every
||sprites:Enemy||
down two pixels
- Challenge: in your own words, explain why this behavior couldn’t be
(easily) handled using only the first
||loops:for ... of||
loop.
It may help to temporarily put the ||loops:pause||
inside that loop to test
The ||loops:for ... of||
statement is sufficient in most all cases
to handle this sort of behavior.
The rest of this appendix is quite a bit more complex, and potentially confusing:
it is included mostly as a reference for use as needed.
The topics introduced are commonly used in the functional programming paradigm.
Concept: Filter and For Each
In JavaScript, there is another common implementation of the ||loops:for each||
loop:
the ||arrays:array.forEach||
function.
This applies the given function to each element in the ||arrays:array||
.
The function that is passed to ||arrays:array.forEach||
is allowed to
have up to two parameters: the first, the element in the array,
and the second, the index of that element in the array.
["Hello", "world"]
.forEach(function (element: string, index: number) {
game.splash(element);
});
There are a number of other functions that can be used in this way.
One of the most common is ||arrays:array.filter||
.
||arrays:Filter||
accepts a function with up to two parameters
(the same allowed in ||arrays:forEach||
) that returns a boolean value.
||arrays:Filter||
then returns a new array containing only the
elements that the given function returned true for.
let myNumbers: number[] = [1, 2, 3, 5, 8, 4];
myNumbers.filter(function (element: number) {
return element <= 4;
}).forEach(function (element: number) {
game.splash(element + " is not greater than 4!");
});
The snippet above will first filter out any elements from the array
||variables:myNumbers||
that are greater than 4,
and then iterate over any remaining elements in the ||arrays:array||
.
Example #2: Filter the Enemies!
- Review the code below
- Identify how
||arrays:filter||
is used to identify only the
||sprites:Enemy||
s on the right side of the screen
- Identify how the
||arrays:forEach||
changes the ||sprites:Enemy||
s
that are on the right half of the screen
for (let i = 0; i < 15; i++) {
let skeleton: Sprite = sprites.create(sprites.castle.skellyFront, SpriteKind.Enemy);
skeleton.x = randint(0, screen.width);
skeleton.y = randint(0, screen.height);
}
controller.left.onEvent(ControllerButtonEvent.Pressed, function () {
let allEnemies: Sprite[] = sprites.allOfKind(SpriteKind.Enemy);
allEnemies.filter(function (element: Sprite) {
return element.x > screen.width / 2;
}).forEach(function (element: Sprite) {
element.x -= screen.width / 2;
});
});
Student Task #2: Move to the Right
- Start with the code from example #2
- Add a new event for when the player presses the
||controller:right||
button
- In this event, use
||arrays:filter||
to select all ||sprites:Enemy||
s
that are on the left half of the screen, and ||arrays:forEach||
to move any
||sprites:Enemy||
s that are on the left to the right side of the screen
(this will be effectively the opposite of the ||controller:left||
button event,
so duplicating that to start will likely be helpful)
- Challenge: recreate the same behavior for
||controller:up||
and
||controller:down||
, so that the enemies can be moved to the top or bottom
half of the screen
What did we learn?
- In your own words, explain how
||loops:for ... of||
loops can be easier to
use than ||loops:for||
loops.
- How do
||arrays:filter||
and ||arrays:forEach||
allow ||arrays:arrays||
to be used more easily?
Sidenote: Arrow Functions
The arrow function (=>
) is very useful when modifying code in cases like this.
This is an alternate form of a function that is not covered in detail in this course,
but allows for clearer formatting when programming in this style.
for (let i = 0; i < 15; i++) {
let skeleton: Sprite = sprites.create(sprites.castle.skellyFront, SpriteKind.Enemy);
skeleton.x = randint(0, screen.width);
skeleton.y = randint(0, screen.height);
}
controller.B.onEvent(ControllerButtonEvent.Pressed, function () {
let allEnemies: Sprite[] = sprites.allOfKind(SpriteKind.Enemy);
allEnemies
.filter(element => element.x > screen.width / 2)
.forEach(element => element.x -= screen.width / 2);
});
The snippet above will behave exactly the same as example #2.
The line element => element.x > screen.width / 2
says
“take the first element,
pass it as a parameter to the function on the right side of the =>
,
and return the result of the statement on the right.
Arrow Functions can also be created that are more than a single line
using curly braces {}
,
but these will not automatically return the result of the statement
and will behave similarly to other ||functions:functions||
.
Additionally, if an arrow function requires more than a single parameter
(or no parameters), parentheses need to be used to group the parameters:
- No parameters:
() => game.splash("I'm here!")
will
splash “I’m here!” when it is called
- Two parameters:
(a, b) => a + b
will return the sum
of ||variables:a||
and ||variables:b||