00:00
00:00
3p0ch
If you like hard games try my Daxolissian System series

plasmid @3p0ch

Cat

Scientist

Read the manual & try stuff

So Cal

Joined on 2/13/10

Level:
13
Exp Points:
1,732 / 1,880
Exp Rank:
38,243
Vote Power:
5.51 votes
Audio Scouts
1
Rank:
Portal Security
Global Rank:
23,489
Blams:
50
Saves:
378
B/P Bonus:
8%
Whistle:
Normal
Medals:
4,873
Supporter:
4y 2m 1d

3p0ch's News

Posted by 3p0ch - August 9th, 2022


I've updated my Godot implementation for the NewGrounds API to handle cloud saves, and using it will be a little tricky especially if you're not used to using a FuncRef so here's the TL DR on how it works. (Admittedly a little rushed because it seems like this would be useful for the Mobile Game Jam if players want to be able to play both from phone and computer and keep their save data.)


NewGrounds' cloud save system will give your game some number of save slots that can each store a string with some maximum size. NewGrounds might change the number of save slots and maximum size over time, so the specific numbers aren't built into the Godot API implementation and you'll want to check your game project's page and look in the API section to see what the current values are.


You'll probably want to make a Dictionary specifically designed to hold all of the data that should be cloudsaved and keep it in an AutoLoad so it's persistent when you change scenes and every script can access it – I'll include an example of the save_handler.gd script I've been using at the end of this post. While the cloud technically just stores a string in each save slot, the procedure for converting an object to a string to transmit over the internet and then be converted back to an object again (known as serializing and deserializing with JSON) is built into Godot and I set up the API implementation to handle that process for you, so you can blithely consider a slot to be something that holds a Dictionary and not worry about those details.


You can send a request to cloud save a variable (like a complex Dictionary) with the command

Ngio.cloud_save(data_to_be_saved, slot_number)

The slot_number is optional and if you leave it out it will save in slot #1. THERE IS NO SLOT #0 – SAVE SLOT NUMBERING STARTS AT #1 and if you use multiple save slots then that will probably mess you up at some point so consider yourself warned. Once you send the request to save the data, it will take some time for the request to be sent and the data to actually be saved. You'll probably want to just send the request and hope that it succeeds without going to inordinate measures to make sure it worked, but if you do want to show some indication that the cloud save was successful you can use a FuncRef like so

Ngio.cloud_save(data_to_be_saved, slot_number, funcref(self, "_on_cloudsave_complete"))

and have an _on_cloudsave_complete() function in your script like this

func _on_cloudsave_complete(result):
	print("in _on_cloudsave_complete with result ", result)

It should accept one parameter that has the results that the NewGrounds API sends from the cloud save attempt. If you don't include that third parameter of a FuncRef when you call Ngio.cloud_save() then by default it will just print that result to the browser's Developer Tools window in the Console tab where you can check it for debugging purposes. Because I know you've been checking there for error messages to debug your HTML5 games this whole time, right?


You can send a request to load cloud data from a single slot with the command

Ngio.cloud_load(funcref(self, "_on_cloud_loaded"), slot_number)

The slot number is again optional and the default is slot #1. Since it will take time for the cloud data to be transferred over the internet and loaded, you can't just have your code stop at that line and wait until the data is loaded and then in the next line of code start using the data (or else it will seem like the game is frozen during the loading process for what could be seconds or longer). Instead you need to have the first parameter of the cloud_load() call be a FuncRef pointing to a function that will handle the saved data once it finally gets in from the internet, and a simple function could be something like

func _on_cloud_loaded(loaded_data):
	my_dict_of_save_data = loaded_data

It should take one parameter which will be the same as whatever you originally sent in your request to save the data. In practice you'll probably want to get more sophisticated and just before calling Ngio.cloud_load() make some visual cue letting the player know that a cloud load is being attempted, and then in the referenced function give the player an indication that the load is complete, and also check that the loaded data is what you expect it to be with something like

if loaded_data == null:
	print("Could not load data from that slot, was anything saved there in the first place?")
elif loaded_data is Dictionary:
	print("Load successful, here's your data: ", loaded_data)
else:
	handle a messup

The results that the NewGrounds API sends from the load request will also show up the browser's Developer Tools window in the Console pane to help with debugging.


You can also send a request to load from all slots with a single command

Ngio.cloud_load_all(funcref(self, "_on_cloud_loaded"))

With a function that handles each slot of data as it comes in like

func _on_cloud_loaded(slot_number, loaded_data):
	if loaded_data == null:
		Could not load data from that slot
	else:
		Do something with the loaded_data from slot_number


If you want to clear the data from a save slot, just use this command because it's simple enough that I didn't bother to write my own function to streamline it.

Ngio.request("CloudSave.clearSlot", {"id": slot_number})

You can also add a FuncRef as a third parameter in that call, but you probably don't need to.


Lastly, by default I made it show results from any API requests to cloudsave in your browser's Developer Tools – Console tab because my Spidey senses tell me that you'll probably need to do some debugging. But once your game is ready to publish, you can (if you like) change show_cloudsave_results near the top of the Ngio.gd script to false so players can't look there to see where the cloudsaved data lives.


And here's the save_handler script I usually use. It includes functions to save locally either in LocalStorage if you're playing from web or your hard drive if you're playing from the Godot editor, and you might want to keep those because saving and loading locally is faster than having to go through the cloud and could be another backup.

extends Node

var key = "3p0ch_cloud_save_test"

var data = {
	"boolean_test": true,
	"int_test": 12,
	"float_test": 4.3,
	"string_test": "testing_funcref_default",
	"array_test": [
		1,
		2.3,
		"four",
		[5, "six"],
	],
	"dictionary_test": {
		"seven": 8,
		9: "ten",
	},
}

func read_save():
	if OS.has_feature('JavaScript'):
		var JSONstr = JavaScript.eval("window.localStorage.getItem('" + key + "');")
		if (JSONstr):
			return parse_json(JSONstr)
		else:
			return null
	else:
		var file = File.new()
		if not file.file_exists("user://" + key + ".save"):
			return null
		file.open("user://" + key + ".save", File.READ)
		return parse_json(file.get_line())

func write_save():
	if OS.has_feature('JavaScript'):
		JavaScript.eval("window.localStorage.setItem(\"" + key + "\", \'" + to_json(data) + "\');")
	else:
		var file = File.new()
		file.open("user://" + key + ".save", File.WRITE)
		file.store_line(to_json(data))
		file.close()

func open_site(url):
	if OS.has_feature('JavaScript'):
		JavaScript.eval("window.open(\"" + url + "\");")
	else:
		print("Could not open site " + url + " without an HTML5 build")

func switchToSite(url):
	if OS.has_feature('JavaScript'):
		JavaScript.eval("window.open(\"" + url + "\", \"_parent\");")
	else:
		print("Could not switch to site " + url + " without an HTML5 build")

Tags:

11

Posted by 3p0ch - July 24th, 2022


If you're reading this, it's probably because you played Smash Peter With Mobile and want to see how to use motion sensing in a mobile game, so without further ado here's the JavaScript code that I originally found could get the accelerometer info if the phone's browser lets it be available. (I'm still not sure how many phones and browsers allow it though, and haven't found any way to get Firefox to read sensors, so also include other ways for the player to interact with your game.)

<html>
  <body bgcolor="white">
    <p id="para">DeviceMotionEvent is not supported by this browser</p>
    <button id="reqPerm" onclick="requestPerm()">Grant permission to use sensors</button>
    <script>
      let html_para = document.getElementById("para");
      let motionOutput = "No motion data received yet";
      
      function requestPerm() {
        DeviceMotionEvent.requestPermission();
      }
      
      window.addEventListener('devicemotion', function(event) {
        motionOutput = "Acceleration along the X-axis: " + event.acceleration.x + "<br>" +
                       "Acceleration along the Y-axis: " + event.acceleration.y + "<br>" +
                       "Acceleration along the Z-axis: " + event.acceleration.z + "<br><br>" +
                       "Rotation along the X-axis: " + event.rotationRate.x + "<br>" +
                       "Rotation along the Y-axis: " + event.rotationRate.y + "<br>" +
                       "Rotation along the Z-axis: " + event.rotationRate.z
      });
      
      function updateFrame() {
        html_para.innerHTML = motionOutput;
        requestAnimationFrame(updateFrame);
      }
      
      updateFrame();
    </script>
  </body>
</html>

Importantly, for Apple devices you need to include a request to use the sensors, and it needs to be made from within a callback from a button press. It can be done with the function DeviceMotionEvent.requestPermission(); which only works on Apple devices that support it. If the device doesn't support it, it won't crash, but it will throw an error to the console. So if you don't want to see it throwing a console error you can do this

if(DeviceMotionEvent.requestPermission) {DeviceMotionEvent.requestPermission();}


In practice my phone and probably most phones don't have gyroscopes or won't let you read them so event.rotationRate will probably not be read, but there is also event.accelerationIncludingGravity which was useful as a way to convert the raw acceleration data from the phone to more useful values -- apparently not all phones report acceleration using the same units, but you can tell how many units gravity is generating and use that as a reference, which I did in Smash Peter.


And if you're a Godot dev, here's the actual source code of the AutoLoad script from Smash Peter that listens to the phone's accelerometer data and lets you use it in-game. Remember that request_permission() needs to be called from within a button click callback (I tried using it in an InputEvent handler checking if the event is an InputEventMouseButton and that didn't work, but making it be from a button's "pressed" event did work).

extends Node

var device_accel:Vector3 = Vector3.ZERO
var device_accel_including_gravity:Vector3 = Vector3.ZERO
var _javascript_callback

func _ready():
	if OS.has_feature("JavaScript"):
		_javascript_callback = JavaScript.create_callback(self, "_on_device_motion")
		JavaScript.get_interface("window").addEventListener("devicemotion", _javascript_callback)

func _on_device_motion(event):
	device_accel.x = event[0].acceleration.x
	device_accel.y = event[0].acceleration.y
	device_accel.z = event[0].acceleration.z
	device_accel_including_gravity.x = event[0].accelerationIncludingGravity.x
	device_accel_including_gravity.y = event[0].accelerationIncludingGravity.y
	device_accel_including_gravity.z = event[0].accelerationIncludingGravity.z

func request_permission():
	JavaScript.eval("if(DeviceMotionEvent.requestPermission) {DeviceMotionEvent.requestPermission();}")

Tags:

1

Posted by 3p0ch - June 22nd, 2022


The beta test build of Daxolissian System: Quad is ready for you to try out, just click that link! All the gameplay mechanics are done, my todo list while you're testing it out and giving me feedback is to find or make music for it, make a preloader screen, implement medals and leaderboards, and make a game completion screen.


One of its features is a gizmo to configure the game to work with your gamepad instead of relying on the usual web-based gamepad implementations that are pretty unreliable, so I'd like to hear from as many people as possible using all sorts of different hardware to see how well this approach works -- if it doesn't work then lemme know your OS (windows / mac / linux), browser, what gamepad you're using, and what happens when you try it. The gamepad mapper is is a tool that I'm making available to the NewGrounds community including source code so game devs can use gamepads much more reliably (hopefully!) in their web games. If a player runs the gamepad configuration gizmo on ANY game on NewGrounds, then their gamepad will be configured for EVERY game on NewGrounds that uses this code without the player ever having to run the configuration thing again. I'll go ask some artists to contribute pictures to the gamepad configuration thing so it'll be more visually appealing if it becomes a NewGrounds thing, so consider that to be something I already plan to change between this beta and the final game.


The 3D action gets intense, so I'm also wondering if most people can play this with laptop-grade hardware or if I need to cut even more corners than I already have to make it computationally efficient enough to be playable from a browser. If it lags or hangs on you, lemme know which level does it and what you were doing at the time.


And it needs music, so if you're a musician who'd like to contribute some sci-fi themed tracks that seem like they fit this type of gameplay then PM me. I'm gonna release the game for free on NewGrounds and don't plan on shooting for Steam or anything so there won't be any money involved, just the publicity of having your music in the game and being in the credits. And if you think you might want to make custom music for it, I'll probably take at least a week or two to check comments from beta testing and polish stuff up before officially publishing.


Tags:

3

Posted by 3p0ch - May 8th, 2022


For anyone using PuzzleScript, it will probably not save players' progress if you upload your game to NewGrounds. Fortunately there's a very easy fix. After you generate the .html file for your game, open it in a text editor to find/replace

document.URL

and replace it with

"a_key_unique_to_your_game_WhichCouldBeInCamelCase or have spaces"


Make sure to include quotes around the thing you replace it with, and use a replacement string that no other game on NewGrounds is likely to use (maybe "your username_your game's name" or something). Then your game should save fine.


Tags:

1

Posted by 3p0ch - April 18th, 2022


For Unity, I made a public static Gamepad class to handle the gamepad stuff and you can call it from the other scripts in your game. Setting it up is a little complicated but it's described in the comments of the file below, which should be saved as Gamepad.cs. After that, the script that was used in the Unity demo shows how the Gamepad class can be called from other scripts.


Gamepad.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;
using Newtonsoft.Json;

// ==================
// === FILE SETUP ===
// ==================
// Save this file as "Gamepad.cs" and include it in your project's Scripts

// Save the following code between /* and */ as "link.xml" in Unity's Assets folder
/*
<linker>
  <assembly fullname="System.Core">
    <type fullname="System.Linq.Expressions.Interpreter.LightLambda" preserve="all" />
  </assembly>
</linker>
*/

// Make an Assets/Plugins subdirectory in Unity
// Go to https://www.nuget.org/packages/Newtonsoft.Json/ and click "Download Package"
// Rename it to newtonsoft.json.##.##.##.zip (instead of .nupkg)
// Put its /lib/netstandard2.0/Newtonsoft.Json.dll file in Assets/Plugins in Unity

// Save the following code between /* and */ as "gampepad.jslib" in Assets/Plugins
/*
mergeInto(LibraryManager.library, {
  ReadGamepadConfig: function() {
    var returnStr = localStorage.getItem("gamepad_configuration");
    var bufferSize = lengthBytesUTF8(returnStr) + 1;
    var buffer = _malloc(bufferSize);
    
    stringToUTF8(returnStr, buffer, bufferSize);
    return buffer;
  },
  
  UpdateGamepad: function() {
    var button_names = ["ButtonUp", "ButtonDown", "ButtonLeft", "ButtonRight",
          "DUp", "DDown", "DLeft", "DRight", "L1", "L2", "R1", "R2", "Start", "Select",
          "LStickPress", "RStickPress"];
    var axis_names = ["LStickUp", "LStickDown", "LStickLeft", "LStickRight",
          "RStickUp", "RStickDown", "RStickLeft", "RStickRight"];
    
    var returnStr = "";
    var gamepadArray = window.navigator.getGamepads();
    
    while ((gamepadArray.length > 0) && (gamepadArray[0] == null)) {
      gamepadArray.shift();
    }
    
    if (gamepadArray.length > 0) {
      returnStr = '{"timestamp":' + gamepadArray[0].timestamp;
      for (var button = 0; button < gamepadArray[0].buttons.length; button++) {
        returnStr += ',"button' + button + '":' + gamepadArray[0].buttons[button].pressed;
      }
      for (var axis = 0; axis < gamepadArray[0].axes.length; axis++) {
        returnStr += ',"axis' + axis + '":' + gamepadArray[0].axes[axis];
      }
      returnStr += '}';
    }
    
    var bufferSize = lengthBytesUTF8(returnStr) + 1;
    var buffer = _malloc(bufferSize);
    
    stringToUTF8(returnStr, buffer, bufferSize);
    return buffer;
  },
  
  OpenGamepadUrl: function(url_ptr) {
    var url = Pointer_stringify(url_ptr);
    window.open(url);
  }
});
*/

// ===============================
// === USING THE GAMEPAD CLASS ===
// ===============================
// This creates a public static class that can be used in any of your other scripts.
// These functions will only be active in the HTML5 build, not when running from the Unity editor,
//   but they will throw Debug.Log notices if you try to check a button or axis name
//   that doesn't exist while you're testing it from the Unity editor.
//
// Gamepad.OpenConfigPage()
// Opens the webpage with the gamepad configuration gizmo
//
// Gamepad.ReadConfig()
// Reads the configuration data from the browser's LocalStorage
// It should be called once, before the gamepad state (which buttons are pressed and
//   where the sticks are) begins to be read
// I recommend having a main menu with a button that can call OpenConfigPage(),
//   and when transitioning from the main menu to gameplay call ReadConfig()
// Alternatively, make the game call ReadConfig() whenever the game regains focus
//   (so it will be read if the player launches the configuration gizmo and
//   then comes back to the game) like so:
//  void OnApplicationFocus() {
//    Gamepad.ReadConfig();
//  }
//
// Gamepad.ReadState()
// Updates the Gamepad class to hold a snapshot of the current state of the gamepad
//   (which buttons are pressed and the values for each axis)
// This should be called once per frame before processing gamepad input
//
// Gamepad.IsPressed(string button)
// Returns true if the button is currently pressed
// See the list of button names below in buttonList
// ButtonUp, ButtonDown, ButtonLeft, and ButtonRight are the cluster on the right of the gamepad
// DUp, DDown, DLeft, and DRight are the D-pad, which is the one part of the gamepad that doesn't work reliably
//
// Gamepad.IsJustPressed(string button)
// Returns true if the button was just pressed on this frame
//
// Gamepad.AxisValue(string axis)
// Returns a float with the value of an axis in the range -1 to 1
// See the list of axis names below in axisList
// LStickLeft should be the negative of LStickRight if the player configured their gamepad correctly
//   so use whichever of Up, Down, Left, Right makes your code easier to understand
//
// Gamepad.buttonList and Gamepad.axisList
// These are arrays of the button and axis names (strings), in case you want to loop through them all
// 
// Gamepad.deadZone
// This is a float; if the absolute value of an axis is less than this, Gamepad.AxisValue() for that axis returns zero
// The default value is set just below here, and it is public so it can be modified by other scripts

public static class Gamepad {
  // Set the sticks' dead zone here
  public static float deadZone = 0.1f;
  
  // Variables to read the gamepad configuration from the player's browser
  private static Dictionary<string, string> config = new Dictionary<string, string>();
  private static Dictionary<string, int> buttonConfig = new Dictionary<string, int>();
  private static Dictionary<string, int> axisConfig = new Dictionary<string, int>();
  private static Dictionary<string, int> axisDirectionConfig = new Dictionary<string, int>();
  
  // Variables to read the current state of the gamepad
  private static Dictionary<string, string> state = new Dictionary<string, string>();
  private static Dictionary<string, bool> previousButtonState = new Dictionary<string, bool>();
  private static Dictionary<string, bool> currentButtonState = new Dictionary<string, bool>();
  private static Dictionary<string, float> currentAxisState = new Dictionary<string, float>();
  
  // Names of buttons and axes
  public static string[] buttonList = {"ButtonUp", "ButtonDown", "ButtonLeft", "ButtonRight",
            "DUp", "DDown", "DLeft", "DRight", "L1", "L2", "R1", "R2", "Start", "Select",
            "LStickPress", "RStickPress"};
  public static string[] axisList = {"LStickUp", "LStickDown", "LStickLeft", "LStickRight",
            "RStickUp", "RStickDown", "RStickLeft", "RStickRight"};
  
  // Conditionally compile this code only if running a WebGL build
  // Otherwise if it's running in the Unity editor, just use functions that do nothing
  //   unless an invalid button or axis name is used, in which case let the dev know with Debug.Log
  #if (UNITY_WEBGL && !UNITY_EDITOR)
    // Import the JavaScript functions from gamepad.jslib
    [DllImport("__Internal")]
    private static extern string ReadGamepadConfig();
    [DllImport("__Internal")]
    private static extern string UpdateGamepad();
    [DllImport("__Internal")]
    private static extern string OpenGamepadUrl(string url);
    
    // Open the webpage with the gamepad configuration gizmo
    // If you have your game hosted on a site other than NewGrounds, the hosting site will also
    //   need to have the gizmo page and this url should be changed to the gizmo page for that site
    // (LocalStorage doesn't allow cross-site data access for security reasons)
    public static void OpenConfigPage() {
      OpenGamepadUrl("https://www.newgrounds.com/portal/view/project/1765261");
    }
    
    // Read the gamepad configuration (imported as strings, which can be null instead of "" or "null")
    public static void ReadConfig() {
      config = JsonConvert.DeserializeObject<Dictionary<string, string>>(ReadGamepadConfig());
      
      // Put the buttons' configuration data into buttonConfig
      foreach (string button in buttonList) {
        if (config.ContainsKey(button) && (config[button] != null)) {
          try {
            buttonConfig[button] = System.Int32.Parse(config[button]);
          } catch (System.FormatException) {
          }
        }
      }
      
      // Put the axes' configuration data into axisConfig and axisDirectionConfig
      foreach (string axis in axisList) {
        if (config.ContainsKey(axis) && (config[axis] != null)) {
          try {
            axisConfig[axis] = System.Int32.Parse(config[axis]);
            axisDirectionConfig[axis] = System.Int32.Parse(config[axis + "_direction"]);
          } catch (System.FormatException) {
          }
        }
      }
      
      // Initialize previousButtonState and currentButtonState to false
      foreach (string button in buttonList) {
        previousButtonState[button] = false;
        currentButtonState[button] = false;
      }
      // Initialize all axes to zero
      foreach (string axis in axisList) {
        currentAxisState[axis] = 0.0f;
      }
    }
    
    // Read the current gamepad state
    public static void ReadState() {
      state = JsonConvert.DeserializeObject<Dictionary<string, string>>(UpdateGamepad());
      
      foreach (string button in buttonList) {
        if (buttonConfig.ContainsKey(button)) {
          previousButtonState[button] = currentButtonState[button];
          if (state.ContainsKey("button" + buttonConfig[button])) {
            currentButtonState[button] = (state["button" + buttonConfig[button]] == "true");
          }
        }
      }
      
      foreach (string axis in axisList) {
        if (axisConfig.ContainsKey(axis)) {
          if (state.ContainsKey("axis" + axisConfig[axis])) {
            currentAxisState[axis] = System.Convert.ToSingle(state["axis" + axisConfig[axis]]) * axisDirectionConfig[axis];
          }
        }
      }
    }
    
    // Return true if a button is pressed currently
    public static bool IsPressed(string button) {
      if (System.Array.Exists(buttonList, b => b == button)) {
        if (buttonConfig.ContainsKey(button)) {
          return currentButtonState[button];
        } else {
          return false;
        }
      }
      Debug.Log("Attempted to use IsPressed on button " + button + " but no button with that name exists.");
      return false;
    }
    
    // Return true if a button was just pressed on the current frame
    public static bool IsJustPressed(string button) {
      if (System.Array.Exists(buttonList, b => b == button)) {
        if (buttonConfig.ContainsKey(button)) {
          return (currentButtonState[button] && !previousButtonState[button]);
        } else {
          return false;
        }
      }
      Debug.Log("Attempted to use IsJustPressed on button " + button + " but no button with that name exists.");
      return false;
    }
    
    // Return the value of an axis
    public static float AxisValue(string axis) {
      if (System.Array.Exists(axisList, a => a == axis)) {
        if (axisConfig.ContainsKey(axis)) {
          if (System.Math.Abs(currentAxisState[axis]) > deadZone) {
            return currentAxisState[axis];
          } else {
            return 0.0f;
          }
        } else {
          return 0.0f;
        }
      }
      Debug.Log("Attempted to use AxisValue on axis " + axis + " but no axis with that name exists.");
      return 0.0f;
    }
  #else
    // This is what gets compiled if the game is run in anything other than an HTML5 build
    // These are pretty much empty, and allow the game to run in the Unity editor without the gamepad
    public static void OpenConfigPage() {}
    public static void ReadConfig() {}
    public static void ReadState() {}
    public static bool IsPressed(string button) {
      if (!System.Array.Exists(buttonList, b => b == button)) {
        Debug.Log("Attempted to use IsPressed on button " + button + " but no button with that name exists.");
      }
      return false;
    }
    public static bool IsJustPressed(string button) {
      if (!System.Array.Exists(buttonList, b => b == button)) {
        Debug.Log("Attempted to use IsJustPressed on button " + button + " but no button with that name exists.");
      }
      return false;
    }
    public static float AxisValue(string axis) {
      if (!System.Array.Exists(axisList, a => a == axis)) {
        Debug.Log("Attempted to use AxisValue on axis " + axis + " but no axis with that name exists.");
      }
      return 0.0f;
    }
  #endif
}


And here's what I had as Textbox.cs which shows how to use the Gamepad class

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem;

// This is an example of a script that uses the Gamepad class
// I recommend having a way from the game's main menu to call Gamepad.OpenConfigPage() to
//   open the webpage with the configuration gizmo
// Then when the player starts the main game, run Gamepad.ReadConfig()
// Or you can do like below and have OnApplicationFocus() calling Gamepad.ReadConfig()
//   so it reads the config if the player launches the configuration page and finishes it
//   and then returns to your game
//
// Each frame, call Gamepad.ReadState() to get the current state of the buttons and axes
//   once before you start checking if buttons are pressed or axes are moved
// This loops through each of the possible buttons and axes
// If you want to check a specific button or axis, do something like
// Gamepad.IsPressed(ButtonRight) -- returns a bool
// Gamepad.AxisValue(LStickRight) -- returns a float from -1 (stick is held left) to 1 (held right)

public class Textbox : MonoBehaviour {
  Text textComponent;
  
  void Start() {
    textComponent = GetComponent<Text>();
    Gamepad.ReadConfig();
  }

  void Update() {
    Gamepad.ReadState();
    textComponent.text = "";
    foreach (string button in Gamepad.buttonList) {
      textComponent.text += "IsPressed(" + button + ") = " + System.Convert.ToString(Gamepad.IsPressed(button)) + "\n";
      textComponent.text += "IsJustPressed(" + button + ") = " + System.Convert.ToString(Gamepad.IsJustPressed(button)) + "\n";
    }
    foreach (string axis in Gamepad.axisList) {
      textComponent.text += "AxisValue(" + axis + ") = " + System.Convert.ToString(Gamepad.AxisValue(axis)) + "\n";
    }
  }
  
  void OnApplicationFocus() {
    Gamepad.ReadConfig();
  }
  
  public void OnButtonPress() {
    Gamepad.OpenConfigPage();
  }
}

Tags:

Posted by 3p0ch - April 3rd, 2022


Just copy/paste the code below and save it in your project as an AutoLoad -- see the instructions within the code.

extends Node

# Save this in your Godot project as gamepad_autoload.gd
# Set it as an autoload in Project - Project Settings - AutoLoad tab
#   and name it GamepadAutoload, and click the Add button
#
# In the button_list and axis_list variables below, specify which actions you want
#   to be associated with which each button or stick direction
# Don't use the D-pad because it doesn't work on my Windows lappy with a Switch
#   controller and I won't be able to play ur game :(
#
# You can set the gamepad actions in Project Settings - Input Map like normal
#   to be able to play the game locally from Godot.
# When the game is exported and run in HTML, it will remove the gamepad settings
#   that you set up to make it work on your system, and will use the gamepad
#   mapper at the game's hosting site.
#
####### Functions and parameters #######
# GamepadAutoload.is_loaded will be true if a gamepad configuration has been
#   loaded and false otherwise. You can use that to style buttons on the main
#   menu (like "Configure gamepad" vs "Re-configure gamepad", make the default
#   focused button be configuration if it's not already configured, etc.)
#
# open_gamepad_config() will open the page for the gamepad configuration gizmo
#   at the website specified in gamepad_config_url below.
#
# read_gamepad_data() will read the gamepad data that's stored on the player's
#   computer after they run the configuration gizmo. It will automamtically
#   run when the game loads and whenever the game regains focus
#   (like if the player just left to the configuration gizmo page
#   and then came back) so you really shouldn't need to call it manually.

# If you publish your game on another site that also has the gamepad mapper
#   available then you'll need to change this address
var gamepad_config_url = "https://www.newgrounds.com/portal/view/project/1765261"

# Set the "actions" arrays below to be the actions in InputMap to
#   link the buttons/axes to
var button_list = [
	{"name": "ButtonUp",    "actions": ["zoom_in"], "button": null},
	{"name": "ButtonDown",  "actions": ["ui_accept", "jump"], "button": null},
	{"name": "ButtonLeft",  "actions": ["zoom_out"], "button": null},
	{"name": "ButtonRight", "actions": ["camera_mode"], "button": null},
	
	{"name": "DUp",    "actions": ["ui_up"], "button": null},
	{"name": "DDown",  "actions": ["ui_down"], "button": null},
	{"name": "DLeft",  "actions": ["ui_left"], "button": null},
	{"name": "DRight", "actions": ["ui_right"], "button": null},
	
	{"name": "L1", "actions": ["fire_missile"], "button": null},
	{"name": "L2", "actions": ["run"], "button": null},
	{"name": "R1", "actions": ["fire_gun"], "button": null},
	{"name": "R2", "actions": ["change_target"], "button": null},
	
	{"name": "Select", "actions": ["select"], "button": null},
	{"name": "Start",  "actions": ["pause", "ui_accept"], "button": null},
	
	{"name": "LStickPress", "actions": [], "button": null},
	{"name": "RStickPress", "actions": [], "button": null},
]

var axis_list = [
	{"name": "LStickUp",    "actions": ["move_up"], "axis": null, "positive": false},
	{"name": "LStickDown",  "actions": ["move_down"], "axis": null, "positive": false},
	{"name": "LStickLeft",  "actions": ["move_left"], "axis": null, "positive": false},
	{"name": "LStickRight", "actions": ["move_right"], "axis": null, "positive": false},

	{"name": "RStickUp",    "actions": ["camera_up"], "axis": null, "positive": false},
	{"name": "RStickDown",  "actions": ["camera_down"], "axis": null, "positive": false},
	{"name": "RStickLeft",  "actions": ["camera_left"], "axis": null, "positive": false},
	{"name": "RStickRight", "actions": ["camera_right"], "axis": null, "positive": false},
]

# You don't need to modify anything from this point on

var is_loaded:bool = false
var gamepad_data = null
var gamepad_key = "gamepad_configuration"
var javascript_callback = JavaScript.create_callback(self, "_on_focus_change")

# Open the webpage with the gamepad configuration gizmo
func open_gamepad_config():
	if OS.has_feature('JavaScript'):
		JavaScript.eval("window.open(\"" + gamepad_config_url + "\");")

# Read the gamepad configuration when the game starts
func _ready():
	if OS.has_feature('JavaScript'):
		JavaScript.get_interface("document").addEventListener("visibilitychange", javascript_callback)
		read_gamepad_data()

# Read the gamepad configuration whenever the game regains focus
#   for example, after running the page with the configuration gizmo
func _on_focus_change(args):
	read_gamepad_data()

# Read the gamepad data if it has been saved
func read_gamepad_data():
	if OS.has_feature('JavaScript'):
		# First clear out all of the gamepad controls from InputMap
		for current_action in InputMap.get_actions():
			for current_input_event in InputMap.get_action_list(current_action):
				if (current_input_event is InputEventJoypadButton) or (current_input_event is InputEventJoypadMotion):
					InputMap.action_erase_event(current_action, current_input_event)
		
		# Read the gamepad configuration from LocalStorage
		gamepad_data = parse_json(JavaScript.eval("window.localStorage.getItem('" + gamepad_key + "');"))
		
		# If the gamepad data was loaded, assign the actions for each button
		if gamepad_data:
			is_loaded = true
			for current_button in button_list:
				if gamepad_data.has(current_button.name):
					if not gamepad_data[current_button.name] == null:
						var new_button = InputEventJoypadButton.new()
						new_button.button_index = gamepad_data[current_button.name]
						new_button.pressed = true
						for action_name in current_button.actions:
							InputMap.action_add_event(action_name, new_button)
			for current_axis in axis_list:
				if gamepad_data.has(current_axis.name):
					if not gamepad_data[current_axis.name] == null:
						var new_axis = InputEventJoypadMotion.new()
						new_axis.axis = abs(gamepad_data[current_axis.name])
						if gamepad_data[current_axis.name + "_direction"] > 0:
							new_axis.axis_value = 1.0
						else:
							new_axis.axis_value = -1.0
						for action_name in current_axis.actions:
							InputMap.action_add_event(action_name, new_axis)

Tags:

1

Posted by 3p0ch - April 3rd, 2022


Here's source code for the demo of using the Gamepad mapper in HaxeFlixel which includes a Gamepad.hx file that should be saved in your root directory next to Main.hx when you're making your game, and the PlayState.hx file which shows how to use it in a game.


Gamepad.hx

package;

import haxe.Json;
import js.Browser;

class Gamepad {
  public static var url = "https://www.newgrounds.com/portal/view/project/1765261";
  public static var key = "gamepad_configuration";
  // Feel free to change the dead zone for axis readings here if you like
  public static var deadZone:Float = 0.1;

  public static var config:Dynamic = null;
  public static var gamepadArray:Array<Dynamic>;
  public static var lastState:Dynamic = {};
  public static var currentState:Dynamic = {};
  
  // These can make it easier to loop through things
  public static var buttonNames:Array<String> = ["ButtonUp", "ButtonDown", "ButtonLeft", "ButtonRight",
      "DUp", "DDown", "DLeft", "DRight", "L1", "L2", "R1", "R2", "Start", "Select",
      "LStickPress", "RStickPress"];
  public static var axisNames:Array<String> = ["LStickUp", "LStickDown", "LStickLeft", "LStickRight",
      "RStickUp", "RStickDown", "RStickLeft", "RStickRight"];
  
  // Read the gamepad configuration from the player's browser memory
  public static function readConfig() {
    config = Json.parse(Browser.window.localStorage.getItem(key));
  }

  // Open the page with the gamepad configuration gizmo
  public static function runConfig() {
    Browser.window.open(url);
  }

  // Call this function to read the current gamepad state
  // Use it at some point during each frame before checking for button presses and axis positions
  public static function update() {
    gamepadArray = Browser.window.navigator.getGamepads();
    // Look for the first gamepadArray index with data
    while ((gamepadArray.length > 0) && (gamepadArray[0] == null)) {
      gamepadArray.shift();
    }
    // If there is an array element with gamepad data,
    // check if it's new compared to the most recent data
    // and only update lastState and currentState if the data is new
    if (gamepadArray.length > 0) {
      if (Reflect.field(gamepadArray[0], "timestamp") == Reflect.field(lastState, "timestamp")) {
        return;
      }
      lastState = currentState;
      currentState = gamepadArray[0];
    } else {
      // If gamepadArray.length reached zero without a gamepadbeing detected, make currentState null
      lastState = currentState;
      currentState = null;
    }
  }

  // Returns true if the indicated button is currently being held down
  public static function isPressed(buttonName:String):Bool {
    if (!buttonNames.contains(buttonName)) {
      trace('Attempted to check isPressed for button $buttonName but there is no button with that name.');
    } else if (Reflect.field( Reflect.field(currentState, "buttons")[Reflect.field(config, buttonName)], "pressed" )) {
      return true;
    }
    return false;
  }

  // Returns true if the indicated button has just been pressed on the latest update
  public static function justPressed(buttonName:String):Bool {
    if (!buttonNames.contains(buttonName)) {
      trace('Attempted to check justPressed for button $buttonName but there is no button with that name.');
    } else if ((Reflect.field( Reflect.field(currentState, "buttons")[Reflect.field(config, buttonName)], "pressed" ))
           && !(Reflect.field( Reflect.field(   lastState, "buttons")[Reflect.field(config, buttonName)], "pressed" ))) {
      return true;
    }
    return false;
  }

  // Returns the value of an axis, only if its absolute value is greater than deadZone
  // Directionality affects the sign, for example if the player is holding the left stick up halfway then
  // Gamepad.axis("LStickUp") would return 0.5
  // Gamepad.axis("LStickDown") would return -0.5
  public static function axis(axisName:String):Float {
    if (!axisNames.contains(axisName)) {
      trace('Attempted to read axis $axisName but there is no axis with that name.');
    } else if (Reflect.field(currentState, "axes")[Reflect.field(config, axisName)] && Reflect.field(config, axisName + "_direction")) {
      if (Math.abs(Reflect.field(currentState, "axes")[Reflect.field(config, axisName)]) > deadZone) {
        return Reflect.field(currentState, "axes")[Reflect.field(config, axisName)] * Reflect.field(config, axisName + "_direction");
      }
    }
    return 0.0;
  }
}


PlayState.hx

package;

// Be sure to import Gamepad.Gamepad
import Gamepad.Gamepad;
import flixel.FlxState;
import flixel.text.FlxText;
import flixel.ui.FlxButton;

class PlayState extends FlxState {
  var configButton:FlxButton;
  var gamepadText:FlxText;
  
  // You should include this in your PlayState.hx so if your game loses and then regains focus
  // (such as if the player ran the gamepad configuration gizmo and now returned to your game)
  // it will read the updated gamepad data from the player's browser
  override public function onFocus() {
    Gamepad.readConfig();
    super.onFocus();
  }
  
  override public function create() {
    // Also be sure to run Gamepad.readConfig() in your PlayState.hx create() function
    Gamepad.readConfig();
    
    // Button to let the player configure their gamepad
    configButton = new FlxButton(10, 10, "Configure", function() {
      Gamepad.runConfig();
    });
    
    gamepadText = new FlxText(10, 50);
    
    add(configButton);
    add(gamepadText);
    
    super.create();
  }

  override public function update(elapsed:Float) {
    // I recommend running Gamepad.update() at the beginning of your PlayState.hx update() function
    Gamepad.update();
    if (Gamepad.currentState == null) {
      gamepadText.text = "No gamepad detected, try plugging in and pressing stuff";
    } else {
      gamepadText.text = "";
      for (axis in Gamepad.axisNames) {
        gamepadText.text += axis + ": " + Gamepad.axis(axis) + "\n";
      }
      gamepadText.text += "\nPressed buttons\n";
      for (button in Gamepad.buttonNames) {
        if (Gamepad.isPressed(button)) {
          gamepadText.text += button + "\n";
        }
      }
    }
    
    super.update(elapsed);
  }
}


The one tricky part is that the gamepad mapper gizmo on NewGrounds will save the gamepad configuration data in Local Storage which is only accessible to webpages running on NewGrounds, so if you're running your game locally using Lime's "test" (so not on NewGrounds) then it won't be able to read the configuration that was set on NewGrounds. So you should copy/paste the gamepad_configuration to Local Storage for your game while it's running locally, and then any time you run your game locally the gamepad will be configured. If you're not already familiar with editing Local Storage directly from your browser, here are super detailed instructions for Firefox and Chrome:


After you've used the Gamepad mapper on NewGrounds, go to any NG page with a game actively running (the Gamepad mapper's page would work) and in your browser's Developer Tools under the tab called Application (on Chrome) or Storage (on Firefox) you should have an option of looking in Local Storage and selecting https://uploads.ungrounded.net. Pick that, then find gamepad_configuration (your browser will probably show a Filter box after you click ungrounded.net under Local Storage, where you can type gamepad_configuration to narrow it down). Right-click on the Value and choose Edit, and you can copy it to your clipboard.


Then while you're running your own game locally to test it, you can go to the Developer Tools tab, Application, Local Storage, whatever it has for your game (probably file://), and click on file:// or whatever it shows. If you're using Firefox: click the + button to the right of the Filter box to add a new item in Local Storage and set its Key to gamepad_configuration and paste the Value in. If you're using Chrome: click on the empty row at the end of the list of Key/Values (which might be the very first row if you haven't been using Local Storage locally), right-click to add a new key called gamepad_configuration, and paste the configuration data as its value. Then reload the page, and it should be able to read the gamepad configuration.


Tags:

1

Posted by 3p0ch - March 5th, 2022


Here's the source code for the tutorial on using the Gamepad mapper in a pure JavaScript game. Some general-use functions that you can copy/paste into your game are included in the beginning, the rest shows how to implement them in practice.

<!DOCTYPE html>
<html>
  <body bgcolor="white">
    <button id="button"></button>
    <p id="para"></p>
    <script>
      
      // **** Here are some generally useful functions ****
      
      // This function attempts to read the gamepad configuration from localStorage
      // It will either return an object with the configuration data,
      //   or null if no gamepad configuration has been saved yet.
      // In your game, you can test whether or not it's null and give the player a
      //   visual cue of whether a gamepad config has been successfully loaded
      //   (like show "Configue gamepad" if not, or "Re-configure gamepad" if so)
      function read_gamepad_config() {
        const gamepad_key = "gamepad_configuration";
        
        return JSON.parse(localStorage.getItem(gamepad_key));
      }
      
      // Open the page on NewGrounds to configure the gamepad
      // The page to configure the gamepad will automatically close itself when it's done
      // So you can listen for when your game's window is focused again and then
      //   attempt to read the gamepad configuration that was just set
      function run_config() {
        const gamepad_mapper_url = "https://www.newgrounds.com/portal/view/project/1765261";
        
        window.open(gamepad_mapper_url);
        window.addEventListener("focus", function(){
          read_gamepad_config();
        }, {"once": true});
      }
      
      // Return true if a gamepad button is pressed
      // If you make your gamepad object and your gamepad configuration global
      //   then you could set up your function to just take button_name
      function is_button_pressed(button_name, gamepad, config) {
        // Make sure button_name exists in config (check for typos of button_name)
        if (button_name in config) {
          if (config[button_name] !== null) {
            return gamepad.buttons[config[button_name]].pressed;
          }
        } else {
          console.log ("Attempted to read button named " + button_name +
              " but that button name is not configured");
        }
        return false;
      }
      
      // Return the value of an axis
      // If you make your gamepad object and your gamepad configuration global
      //   then you could set up your function to just take axis_name
      // If you want to set dead-zones to zero within in this function, feel free
      function axis_value(axis_name, gamepad, config) {
        // Make sure axis_name exists in config (check for typos of button_name)
        if (axis_name in config) {
          if (config[axis_name] !== null) {
            return gamepad.axes[config[axis_name]] * config[axis_name + "_direction"];
          }
        } else {
          console.log ("Attempted to read axis named " + axis_name +
              " but that axis name is not configured");
        }
      }
      
      
      // **** End of generally useful functions ****
      // The rest is demonstration of using them in practice
      
      let html_button = document.getElementById("button");
      let html_para = document.getElementById("para");
      let gamepad_index;
      let gamepad_config;
      let gamepad_object;
      
      let button_names = ["ButtonUp", "ButtonDown", "ButtonLeft", "ButtonRight",
            "DUp", "DDown", "DLeft", "DRight", "L1", "L2", "R1", "R2", "Start", "Select",
            "LStickPress", "RStickPress"];
      let axis_names = ["LStickUp", "LStickDown", "LStickLeft", "LStickRight",
            "RStickUp", "RStickDown", "RStickLeft", "RStickRight"];
      
      // Show this until a gamepad is detected
      html_button.innerHTML = "No gamepad detected, try plugging in and interacting with it";
      
      // Listen for gamepad connection
      window.addEventListener("gamepadconnected", function(event) {
        gamepad_index = event.gamepad.index;
        // Read the config data if it's present
        // Show either "Configure" or "Re-configure" depending on if data has been saved
        if (gamepad_config = read_gamepad_config()) {
          html_button.innerHTML = "Re-configure gamepad";
        } else {
          html_button.innerHTML = "Configure gamepad";
        }
        
        // Add an event listener to configure the gamepad if the HTML button is pressed
        html_button.addEventListener("click", run_config);
        // Enter the main loop
        show_gamepad_status();
      });
      
      // Main loop showing the current buttons pressed and axis values
      function show_gamepad_status() {
        // Only do this if the gamepad is configured
        if (gamepad_config) {
          html_para.innerHTML = "";
          // Get the current gamepad state
          gamepad_object = navigator.getGamepads()[gamepad_index];
          // Show axis status
          for (let axis_name of axis_names) {
            html_para.innerHTML += `${axis_name} ${axis_value(axis_name, gamepad_object, gamepad_config)}<br>`;
          }
          // Show button status
          html_para.innerHTML += "<br>Pressed buttons<br>";
          for (let button_name of button_names) {
            if (is_button_pressed(button_name, gamepad_object, gamepad_config)) {
              html_para.innerHTML += `${button_name}<br>`;
            }
          }
        }
        requestAnimationFrame(show_gamepad_status);
      }
    </script>
  </body>
</html>

The one tricky part is that the gamepad mapper gizmo on NewGrounds will save the gamepad configuration data in Local Storage which is only accessible to webpages running on NewGrounds, so if you're running your game locally from file:// to test it then it won't be able to read the configuration that was set on NewGrounds. So you should copy/paste the gamepad_configuration to Local Storage for your game while it's running locally, and then any time you run your game locally the gamepad will be configured. If you're not already familiar with editing Local Storage directly from your browser, here are super detailed instructions for Firefox and Chrome:


After you've used the Gamepad mapper on NewGrounds, go to any NG page with a game actively running (the Gamepad mapper's page would work) and in your browser's Developer Tools under the tab called Application (on Chrome) or Storage (on Firefox) you should have an option of looking in Local Storage and selecting https://uploads.ungrounded.net. Pick that, then find gamepad_configuration (your browser will probably show a Filter box after you click ungrounded.net under Local Storage, where you can type gamepad_configuration to narrow it down). Right-click on the Value and choose Edit, and you can copy it to your clipboard.


Then while you're running your own game locally to test it, you can go to the Developer Tools tab, Application, Local Storage, whatever it has for your game (probably file://), and click on file:// or whatever it shows. If you're using Firefox: click the + button to the right of the Filter box to add a new item in Local Storage and set its Key to gamepad_configuration and paste the Value in. If you're using Chrome: click on the empty row at the end of the list of Key/Values (which might be the very first row if you haven't been using Local Storage locally), right-click to add a new key called gamepad_configuration, and paste the configuration data as its value. Then reload the page, and it should be able to read the gamepad configuration.


Tags:

Posted by 3p0ch - February 27th, 2022


TL DR: If you're a player, try this out and lemme know how it works with your system – which gamepad and which operating system (Windows / Mac) and browser.

If you're a game dev, I plan to make this open for everyone to use. Let me know if you'd like an implementation for your game engine.


Gamepad support in HTML5 is kinda sux because the JavaScript API behind it doesn't consistently map the same physical gamepad input to the same variables if players are using different gamepads and operating systems. So for my next game I made a scene where if the player has a gamepad they'll be prompted to press each of the buttons and move each of the sticks on the gamepad, and the game will read those inputs and map them to the in-game actions you want. I tested it out with my Switch controller on Windows and it works for all buttons except the left D-pad (which for some reason gives output to an Axis like with a stick instead of to Buttons) but that's fine for my game since the mapping works great for the left and right stick, the four right-side buttons, top and bottom left and right shoulder triggers, start, and select which is plenty.


Even better, I wrote code to save the gamepad configuration to LocalStorage so players will only ever need to configure their gamepad once. Even if they configure it in one game, they can still read that saved configuration data even from a different game, even if it's made in a different game engine. And I ultimately plan to make this public so any game devs on NG can use it in their games.


I'm saving it to LocalStorage as a JSON where the keys are button/axis names in human readable text (LStickDown, R2, ButtonUp [for the right pad's upper button] etc) and the values are the button number that the JavaScript API associates with it (and for axes it also has another key/value indicating whether it's the positive or negative direction). So it should be straightforward to import it into any game engine that can run JavaScript and handle return values, or get a JSON from LocalStorage through any other means. I've already written code for cross-game collabs in Unity, Godot, HaxeFlixel, and Phaser (JavaScript) that could be used to read the values in, so you would just need to handle them in a way that makes sense for your engine/framework.


3

Posted by 3p0ch - January 22nd, 2022


 @OmarShehata mentioned shaders in this thread and for some reason I thought web browsers couldn't use them, but he has a whole tutorial series on it. It turns out shader file sizes are generally tiny enough to go into a preloader screen and could make it look nice and fancy with shader effects, so I learned how to make a shader and put it in the preloader for ur a pixel and here's how.


First download GLSLCanvas by Patricio Gonzalez Vivo from github. You'll just need to get the /dist/GlslCanvas.min.js file and put it in the directory where your HTML5 export is created (where your game's index.html file will be). Or you can do like she says and link to rawgit but I prefer having all the code in my project's distribution.


If you're using something other than Godot then you can glance over the next part to get a sense of how to incorporate a shader into your loading screen but you'll need to tweak it for the html file created by your specific engine or framework. After the part about setting up the html file I'll talk about making the actual shader file and that will be relevant no matter what you're using. Be aware that most game engines will create a new index.html file every time they export and overwrite the old one -- Godot and Unity let you make an HTML template that will be used every time you export so you should modify that template to include the shader, but if you don't have the option of making an HTML template and have to just modify the final index.html file then you might want to also save it under a different file name after you add the shader so you don't accidentally wipe it out if you tweak the game and rebuild the HTML5.


For Godot: if you haven't already set up a custom HTML template, you can get the default one from Godot's website. Put it somewhere in your project's directory (res://) using your computer's file browser (it won't import if you try to add it to the project's file tree from within Godot), then in Godot go to Project – Export and make the Custom Html Shell be that file. Open the template html file in your favorite editor, and where the <body> section starts it has a line where it creates a canvas and you should add the shader just after it like so.

<body>
  <canvas id='canvas'>
    HTML5 canvas appears to be unsupported in the current browser.<br />
    Please try updating or use a different browser.
  </canvas>
  <script type="text/javascript" src="GlslCanvas.min.js"></script>
  <canvas id='shader' class="glslCanvas" data-fragment-url="shader.frag" width=1024 height=600 </canvas>

The <script> line loads the GlslCanvas.min.js file you downloaded earlier (and remembered to put in your HTML5 build directory, right?). The code for your shader will go in the shader.frag file, and you should set the width and height to be your game's viewport size. You can remove the shader when the game starts by adding this shader.remove() line near the end of the html file. (The shader.destroy() function that comes with GLSLCanvas confused the Godot preloader so just use shader.remove().)

        }).then(() => {
          shader.remove();
          setStatusMode('hidden');
          initializing = false;
        }, displayFailureNotice);
      }
    })();
  //]]></script>
</body>
</html>

And lastly to force the shader's canvas to be positioned correctly in the viewport, add the position, top, and left lines to the CSS near the top of the html file

#canvas {
  display: block;
  margin: 0;
  color: white;
  position: fixed;
  top: 0;
  left: 0;
}

Then pop the shader.frag file into your build directory (you can use my code below for testing), make a new HTML5 build of your project so it incorporates the new .html template, and zip it up and upload.


For actually writing the shader code, I learned a lot from https://thebookofshaders.com. I like Visual Studio Code and recommend it; if you write your shader in it, these two VS Code extensions are very useful (you can click the Extensions button on the far left bar and search for them, or they'll come up as options if you save a file with a .frag extension): Shader languages support for VS Code and glsl-canvas. Once you have those installed and activated, you can edit the source code for my shader in Visual Studio Code and press Ctrl+Shift+P and use Show glslCanvas to see the shader in action and confirm that everything's working. When you're writing your own shader that'll also show in real time how the code you're editing will be rendered. Here's the code for the shader from the ur a pixel loading screen, and if you've read the first few sections at thebookofshaders (up to the point where I thought "now I know enough to make what I want, the rest is tl dr") then you can probably follow how I approached the coding. (It might be tempting to put the final blocks of code adding each flying square into a for() loop instead of manually adding all the squares, but I've read that some GPUs have horrible handling of loops so better to avoid them.)

#ifdef GL_ES
precision mediump float;
#endif
#define PI 3.1415926535
uniform vec2 u_resolution;
uniform float u_time;

// Scales a vec2 square size where the "radius" is sq_size in the
// smaller of u_resolution's x or y dimensions
vec2 square_ize(float sq_size) {
  vec2 final_square = vec2(sq_size);
  if (u_resolution.x > u_resolution.y) {
    final_square.x = sq_size * u_resolution.y / u_resolution.x;
  } else {
    final_square.y = sq_size * u_resolution.x / u_resolution.y;
  }
  return final_square;
}

void main() {
  vec2 scaled_position = gl_FragCoord.xy / u_resolution.xy;
  vec3 final_color = vec3(0.0);

  // Determine all of the flying pixels' position, size, and intensity
  vec2 red_sq_pos = vec2(
    fract(0.3 + u_time),
    0.3 * (1.0 + sin(u_time))
  );
  vec2 red_sq_size = square_ize(0.05 + 0.05 * abs(cos(u_time)));
  float red_sq_intensity = 0.2;

  vec2 green_sq_pos = vec2(
    fract(u_time * 1.5),
    0.7 + 0.3 * sin(u_time + 5.0) - 0.2 * fract(u_time * 1.5)
  );
  vec2 green_sq_size = square_ize(0.05 + 0.05 * green_sq_pos.x);
  float green_sq_intensity = 0.2;

  vec2 blue_sq_pos = vec2(
    pow(fract(u_time), 2.0),
    0.4 + 0.3 * cos(u_time) + 0.2 * fract(u_time)
  );
  vec2 blue_sq_size = square_ize(0.075 - 0.025 * blue_sq_pos.x);
  float blue_sq_intensity = 0.2;

  vec2 cyan_sq_pos = vec2(
    sqrt(fract(u_time * 1.7)),
    0.7 + 0.3 * -sin(u_time) - 0.3 * fract(u_time * 1.7)
  );
  vec2 cyan_sq_size = square_ize(0.1 - 0.1 * abs(cyan_sq_pos.x - 0.5));
  float cyan_sq_intensity = 0.2;

  vec2 yellow_sq_pos = vec2(
    0.5 - 8.0 * pow(0.5 - fract(0.8 * u_time), 3.0),
    0.5 + 0.4 * cos(3.5 * u_time)
  );
  vec2 yellow_sq_size = square_ize(0.05 + 0.05 * abs(sin(2.0 * u_time)));
  float yellow_sq_intensity = 0.2;

  vec2 mag_sq_pos = vec2(
    fract(0.6 * u_time + 0.2 * sin(5.0 * u_time)),
    0.5 + 0.2 * cos(7.5 * u_time) + 0.2 * sin(4.0 * u_time)
  );
  vec2 mag_sq_size = square_ize(0.02 + abs(0.08 * sin(u_time * 0.3)));
  float mag_sq_intensity = 0.2;

  // Set overall background
  final_color.r += 0.2 * scaled_position.x * (1.0 + sin(u_time));
  final_color.r += 0.2 * (1.0 - scaled_position.y) * (1.0 + cos(u_time));
  final_color.b += 0.15 * (1.0 - scaled_position.x) * (1.0 + sin(1.3 * u_time));
  final_color.g += 0.2 * clamp(scaled_position.y - scaled_position.x, 0.0, 1.0) * (1.0 + cos(0.3 * u_time));

  // Add red square
  if ((abs(scaled_position - red_sq_pos).x < red_sq_size.x) && 
      (abs(scaled_position - red_sq_pos).y < red_sq_size.y)) {
    final_color.r += red_sq_intensity;
  }

  // Add green square
  if ((abs(scaled_position - green_sq_pos).x < green_sq_size.x) && 
      (abs(scaled_position - green_sq_pos).y < green_sq_size.y)) {
    final_color.g += green_sq_intensity;
  }

  // Add blue square
  if ((abs(scaled_position - blue_sq_pos).x < blue_sq_size.x) && 
      (abs(scaled_position - blue_sq_pos).y < blue_sq_size.y)) {
    final_color.b += blue_sq_intensity;
  }

  // Add cyan square
  if ((abs(scaled_position - cyan_sq_pos).x < cyan_sq_size.x) && 
      (abs(scaled_position - cyan_sq_pos).y < cyan_sq_size.y)) {
    final_color.gb += blue_sq_intensity;
  }

  // Add yellow square
  if ((abs(scaled_position - yellow_sq_pos).x < yellow_sq_size.x) && 
      (abs(scaled_position - yellow_sq_pos).y < yellow_sq_size.y)) {
    final_color.rg += blue_sq_intensity;
  }

  // Add magenta square
  if ((abs(scaled_position - mag_sq_pos).x < mag_sq_size.x) && 
      (abs(scaled_position - mag_sq_pos).y < mag_sq_size.y)) {
    final_color.rb += blue_sq_intensity;
  }

  gl_FragColor = vec4(final_color, 0.5);
}

Tags:

4