Print

Getting xAPI out of a Cocos Creator game and into SLS

How I instrumented a Chinese Language game for the Student Learning Space — the prompts I used, the steps in order, and the two bugs that nearly sank it.


What I started with

Three things sat in one folder on my Mac:

  1. DLXLB_GAME_3B14/ — a Chinese Language game built in Cocos Creator 3.8.7. A Ferris wheel with five cabins; the student describes a rabbit by picking words. 291 files, 59 MB. https://vle.learning.moe.edu.sg/resource/manage?resource=MEDIA&location=MOE&keyword=DLXLB_GAME_3B14.zip&ownerGroups=1061,1174,1163&status=ALL&resourceType=ALL https://vle.learning.moe.edu.sg/moe-library/module/view/approved/e0f4dd4c-4e3e-4ebf-815e-e63b0f37298b/section/108198981/activity/108202454?version=e0f4dd4c-4e3e-4ebf-815e-e63b0f37298b&pageNo=1
  2. https://iwant2study.org/lookangejss/appXapiIntegratorAgent/api/samples/timeline/scorable_newTab_timeline_countable-nouns-are-nouns-that-can-be-counted-with-pictures-replacements-by-acp.zip— a working xAPI package produced earlier by my xAPI Integrator tool, on ordinary HTML5 content. My known-good reference.
  3. Nothing else.

What I wanted was two layers of data in the Learning Record Store: every answer the student submitted — the question, the options, what they clicked — and then a mark.

The game had no xAPI in it at all.


First: why the obvious route doesn't work

My xAPI Integrator injects tracking by reading the page's HTML. On the reference package it found five questions, drag-and-drop interactions and a scoring block, all by inspecting DOM elements.

A Cocos Creator build has no DOM. The entire game is one <canvas>. Run that zip through the integrator and it happily adds the libraries, then tracks nothing but mouse coordinates on a rectangle.

So the work was never "run the tool". It was "write the piece the tool cannot write" — a bridge that sits inside the game's own JavaScript and hands the xAPI library a correct payload.

That reframing came out of the first prompt, and it saved me from a day of wondering why the tool produced an empty package.


The prompts, in order

Prompt 1 — study before you build

file:///Users/…/DLXLB_GAME_3B14/index.html https://iwant2study.org/lookangejss/appXapiIntegratorAgent/public/

please study the game that is in the folder.


I would like to know if it is possible to inject xAPI code in /Users/…/scorable_newTab_timeline_…-by-acp.zip inside the game. xAPI statement need to have two layers. The first layer is what the student clicks in the game, the question, what the student's response was, and then eventually the score given to the student — one mark for each correct, zero for each wrong answer.

 

That prompt was dictated and messy. It did not matter. What mattered is that it carried four things:

Notice what I did not ask for: code. I asked whether it was possible. The study came back with the five question files, the exact class and method where answers are checked, and the news that the integrator wouldn't help. That's a better starting point than a file full of guesses.


 

Handover cue for anyone replicating this: ask for the study first, always. The build is cheap once the study is right.

Prompt 2 — answer the design question honestly

I was asked something I hadn't thought about: the game never lets a wrong answer through. Answer wrongly and it shows 「答案错误,请重试!」, clears your selection, and asks again until you get it right.

Score at the end and every child gets 5 out of 5.

I chose attempt-decayed marking: 1 mark if the first submit is correct, 0.5 on the second, 0 from the third onward. The multi-select question stays all-or-nothing at 1 mark, because that is what the game tells the child.

This is the step most people skip. The scoring rule is a pedagogical decision, not a technical one, and no assistant can make it for you.

Prompt 3 — state the constraint

Do not change the code in the xAPI. I only need you to rewrite the payload. SLS only accepts xAPI in a certain format.

Short, and it shaped everything that followed. lib/xAPI.js comes from the ACP tool; if a future regeneration overwrites it, any edit I made there is lost. So the rule was: the vendor file is read-only, my code lives in a separate file.

Prompt 4 — check before you ship

is the contents of this folder ready to be zipped to be uploaded to SLS to test?




 

It wasn't — the folder was still the raw build, with zero xAPI references in index.html. Asking saved me an upload of a 44 MB package that would have reported nothing.

Prompt 5 — ask for the artefact, named

Please create the necessary files in the folders and zip it up for me, giving it a different name, so that I know this is the file to upload.

The "different name" instruction is small and worth copying. When you're cycling through test uploads, UPLOAD-TO-SLS_DLXLB_3B14_xAPI_v1.zip next to DLXLB_GAME_3B14.zip removes a whole class of mistake.

Prompt 6 — the one that actually mattered

The first version went into SLS and came back wrong. This is what I sent:

无参考答案


 

The xAPI statement only has these five words, it is completely wrong. I went through the entire quiz and clicked through all the questions — xAPI captures it as zero marks for 5 questions. There should also be marks or scores sent but it shows 0.

 
index.js:23 Cannot adjust train position: target position node is not set
xAPI.js:101 [xAPI] Missing agent. Using anonymous fallback.

Three things in that message did the work: what I saw (the literal string), what I did (played the whole quiz), and the raw console output, pasted verbatim. No summarising, no "it seems like". That message produced two precise diagnoses within minutes.


The steps, in order

  1. Put the game folder and the reference xAPI package in one folder, and connect that folder to the session.
  2. Ask for a study. Get back: where the questions live, where answers are checked, and whether your existing tooling applies.
  3. Decide the mark scheme yourself.
  4. State your constraints — which files are off-limits, and why.
  5. Get the bridge file, the vendor libraries and the patched index.html.
  6. Zip from the terminal, not Finder: zip -r -X avoids __MACOSX folders and .DS_Store.
  7. Upload, play the whole activity, and read the console.
  8. Paste what you see straight back, including errors that look unrelated.
  9. Re-zip, re-upload, verify.

What the bridge actually does

The game registers its classes with Cocos, so they can be reached by name and their methods wrapped:

 
js
var F = cc.js.getClassByName('GameLevelFriend');
var origCheck = F.prototype.checkAnswers;

F.prototype.checkAnswers = function () {
  var snap = evaluate(this);          // read the verdict BEFORE the game clears it
  var out = origCheck.apply(this, arguments);
  recordSubmit(this, snap);
  return out;
};

The evaluate(this) call has to run before origCheck, because on a wrong answer the game immediately empties selectedAnswers. Read it afterwards and you get an empty response every time.

At startup the bridge walks Game1.gameLevels, reads each level's question JSON and builds a registry — so the maximum mark is known before the first submit rather than guessed at the end.


Three traps inside the vendor library

These are behaviours in lib/xAPI.js that you cannot fix from outside — only avoid.

A score of 0 is silently replaced. When score <= 0, the library assumes the content failed to report a score and substitutes its own: the count of questions whose last history event was correct. Because this game forces every question right eventually, a genuine zero would have been sent to SLS as full marks.

The fix lives in the data, not the code. After each question is cleared, the bridge appends a closing event whose correct flag reports whether a mark was earned, not whether the final click was right. Now the substituted value is also 0, and 0 is written over 0.

A shorter history is discarded wholesale. Before sending, the library compares your payload against a cached copy in localStorage. If the cached history is longer, it sends the cached object instead — old score and all. So the history array must only ever grow, across reloads and restarts.

Two words poison the payload. A reason containing pause or hidden makes the library return the cached state unconditionally. Use plain reasons like answer-q3-attempt2 and final.


The part I got wrong

My first design assumed the payload alone could carry everything. It could not, and 无参考答案 was SLS telling me so.

That string appears nowhere in the 294 files I uploaded — it is SLS's own placeholder for a missing model answer. The reason it was missing is structural. lib/xAPI.js contains zero occurrences of definition, interactionType or correctResponsesPattern, and both statements it builds use a bare object:

 
js
object: { id: params.activityId }

No interaction definition means no question, no options, no reference answer — no payload can add a field to a statement that has no place to put it.

The fix respects the original constraint. lib/xAPI.js is still untouched; the bridge builds its own statements and posts them through the ADL wrapper the vendor file already configured:

 
object.definition
  interactionType          "choice"
  correctResponsesPattern  ["0[,]1[,]2[,]4"]
  choices                  0 毛灰灰的 · 1 耳朵长长的 · 2 尾巴短短的
                           3 鼻子长长的 · 4 嘴边有长长的胡子
result
  response   "0[,]1[,]2[,]4"    success true    score.raw 1 / 1

And a bug that was entirely mine. The zero marks had a separate cause. My code restored every question found in the cached state as closed, including questions that had been saved while still wrong. Once locked that way, answering correctly could never earn anything — every later submit was treated as a replay. SLS reloads the page when content opens in a new tab, which is precisely the path that poisons that cache, so it failed on the first real test.

Both bugs shared a shape: they only appear on the second run, in the real environment. Neither would have shown up in a single clean pass on my laptop.


The result



 
Q2: 它长什么样子?
Student answer: 毛灰灰的、尾巴短短的、耳朵长长的、嘴边有长长的胡子
✅ Correct (Attempt 1)

Score: 5 / 5
Interactions: 13

t=38s 选择「毛灰灰的」 ✓ · 它长什么样子?
t=41s 选择「尾巴短短的」 ✓ · 它长什么样子?
t=44s 选择「耳朵长长的」 ✓ · 它长什么样子?
t=49s 选择「嘴边有长长的胡子」 ✓ · 它长什么样子?
t=51s 第 1 次确认 · 它长什么样子? → … ✓

Every click timestamped, every submit scored, the reference answer attached. A teacher can now see not just that a child got question 2 right, but that they built the answer over thirteen seconds, one attribute at a time, and never selected 鼻子长长的.


Still open

The console also reported [xAPI] Missing agent. Using anonymous fallback. SLS did not pass an agent parameter, so statements are attributed to a placeholder rather than the student.

That one is not a code problem. SLS only supplies the full parameter set under certain question-type and upload configurations, and the agent cannot be reconstructed from the JWT. Worth resolving before reading anything into the data — a perfectly formed record attached to nobody is still not evidence of learning.


For anyone replicating this

The whole thing — study, build, two rounds of debugging, working package — took an afternoon. Most of that was the two bugs, and both of them were found by pasting a console log into a chat window.

Category: Student Learning Space
Hits: 89