instagram

Students refactor code from a simple animation to include structures within structures, and see how to use nested structures in their own games and animations to manage complexity.

Product Outcomes

  • Students will use nested structures to add complexity to their games

Materials

🔗Nested Structures: Managing Complexity 45 minutes

Overview

Students are introduced to the need for nested data structures, as a way of managing complexity.

Launch

Now that you know all about data structures, you’re able to use them to make video games and animations from scratch, including games that are much more complex than those you worked on in Bootstrap:Algebra.

However, as you add more things to your game, you quickly end up with a large number of elements in your data structure. (If you have multiple characters in your game, each with their own position, speed, costume, etc. that all change, your structure can become quite long and unwieldy.)

Making changes to your structure can get extremely complex, requiring lots of changes to your next-tick and next-state-key functions. One way to manage this complexity is to use nested structures.

For example, if each of our 4 game characters have their own x and y coordinates, we could make one Position structure to use for each character. Then, instead of our game structure containing 8 numbers, it contains 4 Position structures.

Let’s start out with a small animation to explore the benefits of nested structures. Open the Pinwheels 1 Starter File in Pyret, and click "Run". We see four colorful pinwheels spinning in the breeze.

# A PinwheelState is the angle of rotation for 4 pinwheels
data PinwheelState:
  | pinwheel(
      p1a :: Number,
      p2a :: Number,
      p3a :: Number,
      p4a :: Number)
end
STARTING-PINWHEELS = pinwheel(60, 3, 25, 70)

The only things that change in this animation are the angles of rotation for each of the 4 pinwheels.

As usual, we have a next-state-tick function to handle updating the state of the animation, and a draw-state function to draw the animation.

We also have two helper functions to do some of the work for these main functions:

  • update-pinwheel, which increases the angle for an individual pinwheel

  • draw-pinwheel, which rotates the pinwheel image by the given angle.

Our next-state-tick function just calls update-pinwheel for each of the wheels. Most of the work happens in these helper functions!

Suppose we wanted each of the pinwheels to spin at a different speed. We already know that any changeable part of the animation will need to be added to the structure, so means having to add 4 new fields to the PinwheelState structure.

Investigate

Print out the following code screenshot from the pinwheels file and underline or highlight each spot in the code you would need to change in order to add a speed to each pinwheel. Once you’ve identified which sections of the code will need to change, edit the program on the computer so that each pinwheel spins at a different speed.

Now we have a nice animation of pinwheels spinning at different speeds, but this could have been a lot easier if we had started off by making each individual pinwheel its own structure.

Open the Pinwheels 1 Starter File on your computer and take a look at the code. What differences do you see between this starter file and the first?

This time, the PinwheelState data structure contains four structures, instead Numbers. The angle of rotation is now contained inside the Pinwheel structure:

# A Pinwheel is an angle of rotation
data Pinwheel:
  | pw(angle :: Number)
end
# A PinwheelState is 4 Pinwheels
data PinwheelState:
  | pinwheels(
      p1 :: Pinwheel,
      p2 :: Pinwheel,
      p3 :: Pinwheel,
      p4 :: Pinwheel)
end
STARTING-PINWHEELS = pinwheels(pw(60), pw(3), pw(25), pw(70))

  • How would you get p1 out of the STARTING-PINWHEELS instance?

    • STARTING-PINWHEELS.p1

  • What type of data will you get back?

    • pw(60)

  • How would you get p1 out of the STARTING-PINWHEELS instance?

    • STARTING-PINWHEELS.p1.angle

Another change between the non-nested and nested versions of the code is that in the nested version, our helper functions update-pinwheel and draw-pinwheel now take in a Pinwheel data structure, as opposed to just a Number. The animation still works and looks the same on the outside, and the code hasn’t changed too drastically.

Let’s make each pinwheel spin at a different speed.

  • On Nested Pinwheels Code, underline or highlight each spot in the code you would need to change in order to change each pinwheel’s speed independently.

  • Edit the nested version of the program on the computer.

Point out the differences in underlining between the two code screenshots. Note that when students finish this activity, both of the animations will look the same- but one program will have been much more straightforward to modify! We wrote a bit more code at the beginning to set up the nested structures, but that paid off later by giving us more flexibility to change the behavior of the pinwheels.

A side-by-side comparison of the code changes required to add a speed to the non-nested and nested versions of the pinwheels file. The nested version requires far fewer changes.

Just by looking at the differences on paper, we can see the difference in complexity of changing our animations. In order to make each pinwheel spin at a different speed, much more of the non-nested program will need to change, as opposed to the nested version where only the Pinwheel structure, STARTING-PINWHEELS instance, and the update-pinwheel function need to be edited.

What if we wanted to add a breeze to our animation, and make the pinwheels move left across the screen? Let’s assume that each pinwheel moves at the same speed, but each of their x-coordinates will need to change.

Compare Non-Nested Pinwheels and Nested Pinwheels 2, underlining or highlighting the places in the code you would need to edit in order to change the x-coordinates of each pinwheel. Do this for both the nested and non-nested versions of the animation.

For practice, have students make this change in both programs on the computer. Have them pay special attention to their helper functions- will they be able to use the existing update-pinwheel in the non-nested version of the animation?

Synthesize

Compare the updating functions for the non-nested version of the code:

Not-Nested Nested
# update-pinwheel :: Number, Number -> Number
fun update-pinwheel(angle, speed):
  angle + speed
end

# next-state-tick :: PinwheelState -> PinwheelState
fun next-state-tick(ps):
  pinwheel(
    update-pinwheel(ps.p1a, ps.p1speed),
    ps.p1speed,
    ps.p1x - 5,
    update-pinwheel(ps.p2a, ps.p2speed),
    ps.p2speed,
    ps.p2x - 5,
    update-pinwheel(ps.p3a, ps.p3speed),
    ps.p3speed,
    ps.p3x - 5,
    update-pinwheel(ps.p4a, ps.p4speed),
    ps.p4speed,
    ps.p4x - 5)
end
# update-pinwheel :: Pinwheel -> Pinwheel
fun update-pinwheel(p):
  pw(p.angle + p.speed, p.speed, p.x - 5)
end

# next-state-tick :: PinwheelState -> PinwheelState
fun next-state-tick(ps):
  pinwheels(
    update-pinwheel(ps.p1),
    update-pinwheel(ps.p2),
    update-pinwheel(ps.p3),
    update-pinwheel(ps.p4))
end

The nested version is shorter, and it’s also more readable. If each pinwheel moves the same way, we can use one helper function on all of them, each time consuming a pinwheel and producing the updated pinwheel. The only function that needs to change is the one that works with Pinwheel structures (update-pinwheel).

The next-state-tick function doesn’t need to change at all. This offers you lots more flexibility when making changes to your code, or adding things to a program.

How do nested structures work in a complex video game? Here’s the original data block and some sample instances from a video game in which a player tries to catch the target, and avoid a danger:

# A GameState is a Player's x and y-coordinate, danger's x and y
# coordinate and speed, and target's x and y coordinate and speed
data GameState:
    game(
      playerx :: Number, playery :: Number,
      dangerx :: Number, dangery :: Number,
      dangerspeed :: Number,
      targetx :: Number, targety :: Number,
      targetspeed :: Number,
      score :: Number)
end
# Some sample GameStates
START = game(320, 100, 600, 75, 5, 1500, 250, 10, 0)
PLAY  = game(320, 100, 600, 75, 5, 300, 250, 20, 0)

To clean up the GameState structure and allow more flexibility in our code, we defined a new structure to represent a Character, which contains a single set of x and y-coordinates:

# A Character is an x and y-coordinate
data Character:
    char(x :: Number, y :: Number)
end
data GameState:
    game(
      player :: Character,
      danger :: Character, dangerspeed :: Number,
      target :: Character, targetspeed :: Number,
      score :: Number)
end
# Some sample GameStates
START = game(char(320, 100), char(600, 75), 5, char(1500, 250), 10, 0)
PLAY  = game(char(320, 100), char(600, 75), 5, char(300, 250), 20, 0)

For the nested structures version of Ninja Cat:

  • How would you get the player’s x-coordinate out of START?

  • What about the danger’s y-coordinate?

  • How would you get the target’s speed out of PLAY?

  • Finally, what do you notice about these two versions of the Ninja Cat data? Which do you prefer, and why?

Have students discuss the pros and cons of writing a game using nested or non-nested structures.

Now take a look at YOUR video games.

  • If you were to re-write your program using nested structures, what would it look like?

  • Do you have multiple characters in your game with their own x, y, and speed?

  • Do you have any opportunities to use helper functions to move characters in the same way?

For practice, rewrite the data block and sample instances for your video game using nested structures.

If you like, have students completely refactor their entire game code to make use of nested structures and helper functions.

These materials were developed partly through support of the National Science Foundation, (awards 1042210, 1535276, 1648684, 1738598, 2031479, and 1501927). CCbadge Bootstrap by the Bootstrap Community is licensed under a Creative Commons 4.0 Unported License. This license does not grant permission to run training or professional development. Offering training or professional development with materials substantially derived from Bootstrap must be approved in writing by a Bootstrap Director. Permissions beyond the scope of this license, such as to run training, may be available by contacting contact@BootstrapWorld.org.