Log In  
Follow
olivarra1
Follow

I'm trying to make a rhthm-based game, and although the timing looks OK both on pico-8 and in my computer after exported in HTML, when running it through my cellphone everything has ~300ms delay, which ruins the experience.

I've done a very simple cartridge to test the timing - Try to press X to follow the rhythm. Again, on my desktop computer it's fine, but when running it on a cellphone the timing is definetly off.

I've debugged a bit, trying to see if it's some lag added by the browser on cellphones to detect gestures, but the same function that sets the button as on is triggered as soon as it's pressed down. It looks like the delay happens in between the buttons JS variable and pico-8 runtime.

Is there an alternative way of getting input with close to no lag or it's just the browser export? Any way to fix it?

Cart #wozowopema-0 | 2022-02-05 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

P#106396 2022-02-05 20:56 ( Edited 2022-02-05 20:57)

I've made a spike for cartriges to send http requests and receive their response.

The way it works is the cartrige communicates with the browser through gpio. In here I'm using 2 assumptions (I guess dangerous ones) that make it quite simple to send messages:

  1. We get synchronous interrupts from p8 -> JS when p8 writes to a GPIO.
  2. We read an analog value 0-255 in perfect accuracy.

So... yep, this code atm only works in a browser. And it might be posible that it breaks later on (specially for assumption 1, as it depends on the runtime implementation)

It's separated in two layers: The first one just abstracts over sending and receiving any message

html_messaging.lua

html_messaging={
  send_message=function(self, message)
    -- sends a `message` string to the host HTML
  end,

  add_listener=function(self, listener)
    -- adds the `listener` function to be called with receiving messages
  end,
  remove_listener=function(self, listener)
    -- stops sending message updates to the listener
  end,

  update=function(self)
    -- this function must be called on every update, so that html_messaging can check for new messages
  end
}

html_messaging={
  gpio=0x5f80,
  pins={
    send_req=1, -- output
    read_req=3, -- input
    clk_out=5, --input
    data_out=7, -- output
    data_in=8 -- input
  },
  send_message=function(self, message)
    if #message == 0 then return end

    poke(self.gpio + self.pins.send_req, 0xFF)

    for i=1,#message do
      poke(self.gpio + self.pins.data_out, ord(message, i))
    end

    poke(self.gpio + self.pins.send_req, 0x00)
  end,

  _listeners={},
  add_listener=function(self, listener)
    add(self._listeners, listener)
  end,
  remove_listener=function(self, listener)
    del(self._listeners, listener)
  end,
  update=function(self)
    local message = __messaging_receive_message()
    if not message then return end
    for listener in all(self._listeners) do
      listener(message)
    end
  end
}

function __messaging_receive_message()
  local gpio = html_messaging.gpio
  local pins = html_messaging.pins

  local is_sending = peek(gpio + pins.read_req) == 0xff

  if not is_sending then return false end

  local input_buffer = ""
  while peek(gpio + pins.read_req) == 0xff do
    input_buffer = input_buffer..chr(peek(gpio + pins.data_in))
    poke(gpio + pins.clk_out, 0xff)
    poke(gpio + pins.clk_out, 0x00)
  end
  return input_buffer
end

gpio_messaging.js
// This file also sets window.pico8_gpio - Make sure not to override it, or it won't get the interrupts!

window.pico8_gpioMessaging={
  sendMessage(message) {
    // sends the string `message` to p8
    // returns a promise that resolves when the message was completely sent
    // (we don't have synchronous interrupts from js -> p8)
  }
}
// pico8_gpioMessaging is an EventEmitter of 'message' events
// which have a 'message' prop with the received message

(function () {
  class GPIO extends EventTarget {
    static READ = 'read';
    static WRITE = 'write';
    pins = new Array(128);

    get interruptPins() {
      return new Proxy(this, {
        get: (object, prop) => {
          const evt = new ReadEvent(prop);
          this.dispatchEvent(evt);

          return object.pins[prop];
        },
        set: (object, prop, value) => {
          object.pins[prop] = value;

          const evt = new WriteEvent(prop, value);
          this.dispatchEvent(evt);
        }
      });
    }
  }
  window.P8GPIO = GPIO;
  class ReadEvent extends Event {
    pin = 0;

    constructor(pin) {
      super(GPIO.READ);
      this.pin = Number.parseInt(pin);
    }
  }
  class WriteEvent extends Event {
    pin = 0;
    payload = '';

    constructor(pin, payload) {
      super(GPIO.WRITE);
      this.pin = Number.parseInt(pin);
      this.payload = payload;
    }
  }

  const gpio = new GPIO();
  window.pico8_gpioBus = gpio;
  window.pico8_gpio = gpio.interruptPins;

  const HIGH = 0xFF;
  const LOW = 0x00;
  const PINS = {
    sendReq: 1, // input
    readReq: 3, // output
    clkIn: 5, // output
    dataIn: 7, // input
    dataOut: 8 // output
  }

  class GPIOMessaging extends EventTarget {
    static MESSAGE = 'message';

    constructor() {
      super();
      gpio.addEventListener(GPIO.WRITE, event => {
        if (event.pin === PINS.sendReq && event.payload === HIGH) {
          this._receiveMessage();
        }
      })
    }

    _messageQueue = new Array();
    sendMessage(message) {
      const { resolve, promise } = destructPromise();
      this._messageQueue.push({
        resolve,
        message
      });
      this._flushMessages();

      return promise;
    }

    _isSending = false;
    _flushMessages() {
      if (this._isSending || this._messageQueue.length === 0) return;

      const { resolve, message } = this._messageQueue.shift();
      if(message.length === 0) {
        return resolve();
      }
      this._isSending = true;

      let idx = 0;
      const self = this;
      function handleWrite({ pin, payload }) {
        if(pin === PINS.clkIn && payload === LOW) {
          idx++;
          if (message.length <= idx) {
            gpio.removeEventListener(GPIO.WRITE, handleWrite);
            gpio.pins[PINS.readReq] = LOW
            resolve();
            self._isSending = false;
            self._flushMessages();
          } else {
            gpio.pins[PINS.dataOut] = message.charCodeAt(idx)
          }
        }
      }
      gpio.addEventListener(GPIO.WRITE, handleWrite);

      gpio.pins[PINS.dataOut] = message.charCodeAt(idx)
      gpio.pins[PINS.readReq] = HIGH
    }

    _receiveMessage() {
      let buffer = "";
      const _self = this;
      function handleWrite({ pin, payload }) {
        switch (pin) {
          case PINS.dataIn:
            buffer += String.fromCharCode(payload);
            break;
          case PINS.sendReq:
            if (payload === LOW) {
              gpio.removeEventListener(GPIO.WRITE, handleWrite);
              const event = new MessageEvent(buffer);
              _self.dispatchEvent(event);
            }
            break;
        }
      }
      gpio.addEventListener(GPIO.WRITE, handleWrite);
      // In here we told pico8 we were ready to receive
      // Now that's not needed anymore, as p8 assumes interrupts are synchronous
      // gpio.pins[2] = HIGH
    }
  }
  window.P8GPIOMessaging = GPIOMessaging;
  class MessageEvent extends Event {
    message = ''
    constructor(message) {
      super(GPIOMessaging.MESSAGE)
      this.message = message;
    }
  }
  window.pico8_gpioMessaging = new GPIOMessaging();

  function destructPromise() {
    let resolve, reject
    const promise = new Promise((res, rej) => {
      resolve = res;
      reject = rej;
    });
    return { resolve, reject, promise };
  }
})();

The other layer needs to have that one imported first - This is the one to send and receive HTTP requests

http_request.lua

http_request={
  get=function(self,url,cb)
    -- sends a GET request to url, sending the message as a string to callback once it's received
  end
}

-- requires html_messaging.lua

http_request={
  _cid=0,
  get=function(self,url,cb)
    local cid = self._cid
    self._cid = self._cid + 1
    local handle_message = function(message)
      local match_header = "HTTP RES "..cid
      local msg_header = sub(message, 1, #match_header)
      if match_header != msg_header then return end
      local response = sub(message, #match_header + 2)
      cb(response)
      html_messaging:remove_listener(handle_message)
    end
    html_messaging:add_listener(handle_message)
    html_messaging:send_message("HTTP REQ "..cid.." GET "..url)
  end
}

http_request.js

// Exports nothing: This file listens for p8 http requests and proxies them to the real BE

// Requires gpio_messaging.js

pico8_gpioMessaging.addEventListener('message', ({ message }) => {
  if (message.startsWith("HTTP REQ")) {
    const [http, req, cid, method, url] = message.split(" ");
    fetch(url, {
      method
    })
      .then(result => result.text())
      .then(result => pico8_gpioMessaging.sendMessage(`HTTP RES ${cid} ${result}`))
  }
})

I can't embed an example cartrige here because it also needs some setup on the HTML/JS side, but a simple script that requests the datetime from worldclockapi:

#include html_messaging.lua
#include http_request.lua

function _update()
  if btnp(🅾️) then
    http_request:get("http://worldclockapi.com/api/json/utc/now",
      function (response)
        printh(response)
      end
    )
  end

  html_messaging:update()
end

The only thing the host HTML needs is importing both gpio_messaging.js and http_request.js.

Now I only need to write a JSON parser :'D (/s - I know this would be a bad idea).

I think this might be useful for simple ad-hoc services, like global highscores. Also maybe to run around the size limit in cartridge for things like level data, or maybe even to some extend turn-based multiplayer games? :exploding_head:

P#90761 2021-04-18 09:19

Cart #zujewipewe-6 | 2021-04-13 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
8

I present my very first game in pico-8 - Inspired by a predecessor of the very well known Mine Sweeper, a game for the ZX Spectrum called "Mined Out!"

In this game you must reach an objective (the bottom right corner) without stepping on a mine. As you move, you'll get information of how many mines are around you.

The level selector (0 -> 9) only the number of mines added, the size of the board and a score multiplier. The position of the mines is completely random.
It also uses a seed system so you can share the same RNG with another player so you can compete in the same conditions.

P#90301 2021-04-09 21:40 ( Edited 2021-04-13 21:39)

Follow Lexaloffle:        
Generated 2022-06-29 13:13:18 | 0.061s | Q:15