RE: Building E-Learning Games - Prototype Development #471

Follow me as I create a Storyline gamified e-learning project in response to the Building E-Learning Games challenge on the E-Learning Heroes forum.

RE: Building E-Learning Games - Prototype Development #471
tl;dr: Click here to skip to the latest version 👇

Day 1 of Prototype Development

Gathering Resources & Inspiration

Because the build for this could be challenging, and I don't have a clear idea yet on how I'm going to approach it, I've spent some time pulling together resources which could give me some hints on which way to go. Here are my favourites:

Falling object Game in JavaScript

This Codepen by Notjess was a lot of fun! Playing around with this interaction encouraged me I was on the right track because it was simple yet somewhat addictive!

Galaga-Style Retro Game

I also found this by Lindsay O'Neill, built in Storyline, I haven't watched this yet but I'm keeping it in my back pocket!

Spicey Yoghurt

Over the past few days I've read through this whole tutorial by Spicey Yoghurt, it's a little off topic but if I can't use Storyline's built in features, perhaps I could inject a Canvas and use Storyline as a kind of wrapper around a JavaScript game, so I can export it easily to an LMS.
https://spicyyoghurt.com/tutorials/html5-javascript-game-development/create-a-proper-game-loop-with-requestanimationframe

Learnomancer

Fantastic in-depth tutorial by Learnomancer about animation in Storyline with GSAP.

Development

The core problem we're tackling in this challenge is enhancing our learners' recall of each country's capital city. That's the cornerstone of our focus and the knowledge we aim to improve. As long as what we build achieves this it's okay to refine the idea as we test right?

The first thing I tested was the thing I thought would be most complex. Dynamically matching pairs of items. I realised I could simplify the game, the build and improve the gameplay by dropping the time element. Instead of having a timer counting down, the time element comes from the speed at which the items fall, and how quickly our user can match the items.

I've introduced a simple scoring system that can be refined as we progress. For now, you earn 10 points for a correct answer and lose 10 points for an incorrect one. In the future, we could enhance this by adding bonuses for matches made within a certain amount of time, among other possibilities.

Here's how it's looking so far:

0:00
/0:26

JavaScript in Storyline

I've decided to attempt the complex part of this project with JavaScript primarily, rather than Storyline triggers. While triggers might get us up and running quickly, they could become quite difficult to manage over time for a game like this. Instead of managing each object's individual triggers for clicks, hovers, and correct answers, I used JavaScript for the heavy lifting, and Storyline for the visual layout.

One Huge Advantage

After I had a written the JavaScript for this I realised how much faster it enabled me to work and adapt the game. All I have to do is add or remove a shape and change the accessibility attribute and that's it! JavaScript does the rest in the background, applying all the 'triggers' (Event Listeners) and actions automatically as I add and remove shapes.

The Code

If you're interested in getting into the detail, here's the code that makes this all work:

// Setting up variables

let score = 0;
let clickedItem1 = null;
let clickedItem2 = null;

// Get all elements where the value for  attribute data-acc-text starts with fo- (falling object)
const items = document.querySelectorAll("[data-acc-text^='fo-']");

// Getting the Storyline player so we have access to Storyline functions. This allows us to communicate with Storyline and set the score.

const player = GetPlayer();

// Get the country code from the end of the accessibility string stored in the 'data-acc-text'attribute. Example: fo-united-kingdom:GB

function getCountryCode(str) {
  const parts = str.split(":");
  console.log(`Country Code: ${parts[parts.length - 1]}`);
  return parts[parts.length - 1];
}

// Check if the country codes match

function matchItems(item1, item2) {
  return getCountryCode(item1) === getCountryCode(item2);
}

// Get the accessibility attribute of the cliked element so that we can test it

function getClickedAttr(element) {
  return element.getAttribute("data-acc-text");
}

//handles stores the items which are clicked and if both two items have been clicked, checks to see if they match

function handleClick(event, player) {
  const targetElement = event.target.closest("[data-acc-text]");
  if (targetElement) {
    if (!clickedItem1) {
      // If there is no clicked item save this item and return.
      clickedItem1 = getClickedAttr(targetElement);
      console.log(`You clicked: ${clickedItem1} first.`);
    } else {

      clickedItem2 = getClickedAttr(targetElement); // If this is the second click, call the match function to check if they match
      console.log(`Then you clicked: ${clickedItem2}`);

      if (matchItems(clickedItem1, clickedItem2)) {

        // if they match update the score
        score += 10;

      } else {

        // If they don't deduct points
        score -= 10;

      }

      console.log(`Score is: ${score}`);
      player.SetVar("Score", score); // Update the Storylinen variable with the new score
      clickedItem1 = null; // Reset the clicked items
      clickedItem2 = null;

    }

  } else {
    console.warn("Clicked element does not have data-acc-text");
  }

}


// Handles the hover in and out so we know the object is clickable (Storyline can't do this for us since it doesn't know it's a trigger);

function handleMouseOver(event) {
  event.target.style.cursor = "pointer";
}


function handleMouseOut(event) {
  event.target.style.cursor = "default";
}

// Helpful for debugging and checking all of the items have acc attributes set

function printAccNames(items) {
  items.forEach((item) => {
    console.log(item.getAttribute("data-acc-text"));
  });

}

// Initialise the logic when Storyline calls the script. Currently when the timeline starts

function init(items, player) {
  try {
    if (items.length > 0) {
      // only set this script up if there are items (the words which drop) on the slide
      console.log("Items found:", items);
      items.forEach((item) => {
        item.addEventListener("mouseover", handleMouseOver);
        item.addEventListener("mouseout", handleMouseOut);
        item.addEventListener("click", (event) => {
          handleClick(event, player);
        });
      });

    } else {
      console.warn("No items found.");
    }

  } catch (error) {
    console.error("An error occurred while retrieving items:", error);
  }

  // Prints a list of the data-acc-text set in Storyline UI under accessibility

  printAccNames(items);

}

  

init(items, player); // initialise the script, passing in the SL player and the items.

I'll continue to work on this and share my thinking as I go, check back here for more!

Day 2 of Prototype Development

Thanks to the incredibly powerful GSAP animation library, I've implemented the falling animation without much difficulty. It has already made the game far more fun and challenging. However, I wonder if it might be too challenging at this point. Are we testing the learners' ability to click quickly, in addition to recalling which city goes with which country? Is this okay in addition to testing recall? I'm not sure yet.

0:00
/0:12

Further improvements

Okay it's a few hours later and I'm feeling much better about the idea! It feels very slick with the tweaked animation settings. I've also:

  • Prevented clicking the same item twice, which would result in points being added incorrectly
  • Hidden the items after a correct combination
  • Added a selected variable so we can see what is selected
  • Added a Class name to each item using the init() function

That last point, adding a class name to each item, allowed me to easily animate each by selecting every item with the new class name of 'animate-down'.

💡
After dynamically adding the class name with JavaScript, I realised I could refine the script by minimising the use of accessibility attributes. This way, we maintain the original purpose of these attribute, ensuring accessibility, without unnecessary interference.

Day 2 version:

0:00
/0:15

Pretty cool right? Next I'll work on adding the other pieces of the interface and user experience in Storyline. I'm not quite ready to add visual styles yet, just the rest of the wireframe to get a feel for how it works overall.

GSAP Animation JavaScript

This is the GSAP animation code I used in Storyline, after lots of tweaking I think this combination of stagger and the duration works best. The function is called immediately here by the Storyline trigger, but alternatively we could add this as an event on a button so the learner has control over when the game starts.

let slide = document.querySelector("#slide");

let tween = gsap.to(".animate-down", {
  duration: 10,
  y: () => slide.offsetHeight, // animate by the px height of the slide
  yPercent: -100, // offset by the width of the box
  ease: "none",
  paused: true,
  stagger: {
    // wrap advanced options in an object
    each: 1.2,
    from: "center",
    grid: "auto",
    ease: "none",
    repeat: -1, // Repeats immediately, not waiting for the other staggered animations to finish
  },
});

tween.play();

Day 3

Day three version

0:00
/0:23

I'm going to call this prototype phase complete! Today I did the following:

  • Setup arrays to record correct and incorrect answers.
  • Setup callback function for GSAP to call when animation completes so we know the level is over
  • Call Storyline player.SetVar to let Storyline know the level is complete
  • Setup up a check to end the game if all correct answers are given
  • Added basic menu items and feedback
I've been looking at the other prototypes on the challenge post and some of you have really nailed the Gameshow theme! As you can probably see I haven't focused at all on the visuals, so I have my work cut out for me next week to catch up. See you in the Production and Development week!

Day 4 / 5

Latest Version

0:00
/0:27

Alright, I thought I was done, but I've spent another two days refining the prototype. These new additions significantly enhance its value and replayability.

I've added the following:

  • Score multipliers, you now get more points if you answer quickly
  • Scores are now in x100 because it just feels more satisfying
  • Feedback for the learner on the last slide with all the correct and incorrect answers
  • I've shuffled the positions so we start with a different city and country pair each time. In the video, it was just by chance that these matching pairs were together!
  • I did write my own shuffle function for which item dropped first too but it turns out I didn't need to, because GSAP includes a grid property. Now the items fall from a ranom location instead of the same central point.
  • Instead of preventing clicking the same item twice, if you click it twice you can now deselect an item if you decide you want to go after another pairing

All of these improvements should extend the life of the game, and the best part, no need to add or take away triggers, we can just add and remove items and it will continue to work.