Questa settimana ho completato il corso su Svelte e ho cominciato a sperimentare. Mi piacerebbe convertire questo blog e il repository dei miei template (questo) in un sito basato su SvelteKit. Ci sono però alcuni problemi legati alla disposizione delle cartelle nel repository. Quindi per il momento non ho ancora deciso come procedere.

animation

Allora ho deciso di provare qualcosa di diverso e di provare un progetto più limitato. Ho voluto capire come far comunicare una pagina html con un progetto Construct 3 inserito nella pagina stessa. La mia idea è di esplorare la possibilità di integrare in maniera trasparente dei piccoli giochi all’interno di un progetto più esteso usando C3 per la parte dinamica e Svelte per la gestione dei dati. Per provare la mia idea ho scelto un gioco molto semplice, Petits Chevaux:

animation

La parte centrale, quella con i cerchi concentrici e le palline che girano è creata con C3. Ma il pulsante sulla destra e la tabella sulla sinistra è puro HTML. I dati vengono passati da e verso C3 tramite l’API Window.postMessage().

Comincio con il codice di Construct 3. Nel file main.js inserisco una funzione attachListeners

import { attachListeners } from "./postMessage.js";

runOnStartup(async runtime => {
	globalThis.g_runtime  =  runtime;
	runtime.addEventListener("beforeprojectstart", () => attachListeners());
});

La funzione serve per creare un’interfaccia per permettere la comunicazione da e per la pagina che ospita il gioco.

Il file postMessage.js contiene alcune funzioni. Le principali sono 3:

  • attachListeners
  • getMessage
  • sendMessage

attachListeners aggiunge un semplice osservatore all’evento message

function attachListeners() {
	if (globalThis.addEventListener) {
		globalThis.addEventListener("message", getMessage, false);
	} else {
		globalThis.attachEvent("onmessage", getMessage);
	}
}

Quando la pagina riceve un evento message esegue la funzione getMessage:

function getMessage(e) {
	if( !trustedOrigin.includes(e.origin)) {
		console.log("Error, wrong origin");
	} else {
		const message = e.data;
		const messageType = message.type;
		const messageContent = message.content;

		match(messageType)
      .on(t => t.toLowerCase() === "set", () => getMessageSet(messageContent))
		  .on(t => t.toLowerCase() === "status", () => getMessagePlay(messageContent))
      .otherwise(t => () => 0);
	}
}

L’evento contiene molte informazioni, le più utili in questo caso sono event.origin ed event.data. event.origin ci permette di controllare la pagina che ha originato il messaggio. Se è una pagina considerata attendibile allora leggiamo il contenuto del messaggio e decidiamo cosa fare. Il messaggio è contenuto in event.data. Per semplificarmi la vita ho deciso di usare uno standard per i messaggi inviati e ricevuti. Sono tutti degli oggetti con proprietà type e content.

La terza funzione è sendMessage:

function sendMessage(message) {
	const targetWindow = globalThis.parent;
	targetWindow.postMessage(message, "*");
}

Tramite questa funzione possiamo spedire un messaggio a un’altra pagina, a un’altra finestra oppure a un elemento iFrame. Poiché userò il gioco all’interno di un iFrame è sensato spedire il messaggio a Window.parent, ovvero la pagina che contiene l’iFrame con il codice del gioco. Conviene però usare globalThis per garantire una maggiore compatibilità con le caratteristiche di Construct 3.

Sistemato il codice di C3 è il momento di passare a quello della pagina che ospiterà il gioco. Il codice é sostanzialmente lo stesso con una piccola differenza: sendMessage invierà il messaggio a un iFrame.

export interface Message {
    type: string;
    content: string;
}

export function sendMessage(iframe: HTMLIFrameElement, 
                            message: Message, 
                            targetOrigin: string = "*") {
    iframe.contentWindow.postMessage(message, targetOrigin);
}

Poiché sto usando Svelte ho creato un componente Construct.svelte:

<script lang="ts">
    export let construct3: HTMLIFrameElement;
    const src = "./c3/game.html"
</script>

<iframe title="C3" bind:this="{construct3}" {src} scrolling="no" noresize="noresize" /> 

Per permettere all’applicazione di leggere l’iFrame ho inserito un prop construct3. In App.svelte scrivo:

<script lang="ts">
  import { onMount } from "svelte";
  import { attachListeners, sendMessage } from "./PostMessage/postMessage";

  import Construct from "./Construct/Construct.svelte";

  let construct3: HTMLIFrameElement;

  onMount(() => {
    attachListeners();
  });
</script>

<Construct bind:construct3 />

In questo modo la variabile construct3 è collegata all’iFrame del gioco e posso usarla per spedire dei comandi. Posso, per esempio, far eseguire un nuovo giro di ruota premendo un pulsante usando un codice simile a questo:

<script>
  function spin() {
    sendMessage(construct3, {
      type: "status",
      content: "SPIN"
    }, "*");
  }
</script>

<button  on:click={spin}>Spin</button>

Per mostrare il codice che ho usato ho creato un nuovo repository (el3um4s/petits-chevaux) e una pagina su Itch.io.

Il codice del file Construct 3 è caricato anche sul solito repository di GitHub