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,638 / 1,880
Exp Rank:
38,468
Vote Power:
5.48 votes
Audio Scouts
1
Rank:
Portal Security
Global Rank:
23,697
Blams:
50
Saves:
371
B/P Bonus:
8%
Whistle:
Normal
Medals:
4,611
Supporter:
3y 2m 23d

3p0ch's News

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

Posted by 3p0ch - October 3rd, 2021


If you're thinking about getting into making a web game but aren't sure where to start, you've come to the right place. Especially if you're poor and want to do it on a budget of $0. Currently it's very possible to do with a number of different approaches depending on what path you think is best for your specific situation – how much you already know about programming, and whether you just want to doodle around for fun or want to get into a career semi-related to what you're doing. (Alas, I don't have the secret of how to make a ton of cash as an indy game dev and avoid being a working stiff, so don't quit your day job just yet.) This guide is by no means the only way to get started but is what I considered sound advice worth sharing.


If you don't already have programming experience, then your first decision will be whether you want to learn how to do the programming for your game, or whether you're fine just designing the game mechanics and bringing your ideas to a programmer to breathe life into. The latter approach might be easily doable if you're designing a point-n-click, or RPG type adventure, or certain types of puzzle games (especially ones where you can work through the puzzle aspects with pencil and paper to see that they're at an appropriate difficulty). For action games where you're going to need to do a lot of tweaking of parameters by playtesting it until things feel right (movement speed, damage per bullet, turn radius of homing missiles, jump height) it would be easier if the designer is also doing the programming and tweaking stuff on the fly, but it would be possible to have a programmer set up the game and tell the designer to download the game engine and run it locally and show you where the parameters live and can be tweaked. But that would mean you'll need to find a programmer willing to work on your game. Do a lot of the game design work up front and go to a programmer once you have a reasonably detailed plan for the game; otherwise no one will want to program a half-baked idea, and even if they did they wouldn't be able to program it until the design is fleshed out.


If you want to do the programming yourself but don't have programming experience, your next decision will be whether to try to learn some programming before diving into a game engine and figuring things out from their docs and tutorials. It's not required, but if you have the patience to learn to program in ANY language (not necessarily the language implemented by the game engine that you'll use) then things will be much easier to follow and you'll have much more flexibility to make your game do what you want. How much programming should you know before diving into a game engine? The more the better, but things that would be of practical use are if you know what variables and arrays are and how to use them, conditional (if-else) and looping flow controls, how to make functions that take parameters and return values (and the dangers of misunderstanding whether you're passing by value versus passing by reference), and the basics of object oriented programming (have a sense of what a class definition versus an instance is, and how to invoke instances and work with instance variables and methods). If you feel like you can write a program that calculates how long the longest streak of consecutive increasing cards is likely to be if you shuffle a deck of cards and flip then over one-by-one, and especially if you write the program by making a deck object and declaring functions to do major operations (like shuffling, flipping cards, and having a big outer loop that gets called 1000 times so you can get a reasonable average after a lot of runs) then you're definitely well prepared. You don't really NEED to know all that, and you're probably fine if you can handle variables, if-else conditionals, and loops, but the more you know the easier things will be.


Again, it's not required to know how to program before diving into game making, but if you want to learn how to program then some good places to learn are: MDN tutorials which go over all things web related from HTML to CSS to JavaScript, which are handy to know if you're making stuff for the web regardless of what engine you use (JavaScript is the programming part of it, you can skip the HTML and CSS if you want). That tutorial series also seemed like it would be the easiest one to follow for someone who has little to no prior programming experience. Other options would be learning C# (which Unity uses) from Microsoft's docs, or Python (particularly from this online book for games if you're completely new to programming) which might be easier to learn and is essentially the language Godot Engine uses. You'll probably want to install Visual Studio Code as a code editor and get used to using it relatively early in the process of learning how to program, unless you stick with Python / Godot since Godot has its own built-in code editor that's better at interacting with the IDE than if you used Visual Studio Code.


Now that you've either learned all the programming you want to learn or decided you're too cool for school, you're about ready to get started on your first game. I would recommend to FIRST come up with the overall plan for your game's design, THEN go download and learn how to use whichever game engine you pick. The reason is because when you go through the tutorials for the game engine, you shouldn't just go through them and essentially copy/paste to recreate the tutorial game. You should really understand what they're showing you how to do and why it's useful, and a great way of doing that is to have a project in mind and as you're reading the tutorial say "Oh, that will be useful when I'm making my game, I should remember how to do that and know how to adapt it to do what I want." Your game design will likely change as you see what sorts of things can and can't be done easily, which is fine, and you'll probably also get a sense of how complicated a game really is (not just the main play screen but also a HUD that shows your HP and ammo or whatever, a title screen with level selection, a pause screen with options, a game over screen, transitions between levels, and a lot more that you haven't thought of yet). This is why everyone always says to keep your first game simple, because it will end up being a lot more complicated than you originally think.


Now for the game engines, it's time to decide which one to start with. Don't get too bent out of shape about making the right choice – if you download an engine and either can't figure out how to use it or find out it can't do what you want or for whatever reason decide it's not right for you, just trash it and get another one. Even if you get good at using one engine and decide to switch engines later on, what you learn from game design in one engine will help you to use any other. The game engines that I'm familiar with (there are others out there but I'm not talking about them because I'm not familiar with them) are:


GDevelop. Did you decide that programming is for nerds and you're going to jump into game making without knowing anything about it? Then GDevelop is your best bet, at least out of the engines I've used; Construct and Game Maker Studio are also options but I haven't used them because I'm sticking to my $0 budget, and there's Scratch but I've seen too much Scratch cringe to want to use it. GDevelop uses visual scripting, which is similar in spirit to traditional coding but is designed to be easier to grasp by anyone without programming experience. I should give a disclaimer that I haven't made a full game of my own design in it because after going through the tutorials I decided against it, but I feel I know enough to discuss it. It will be more limited in what it can do than the other engines, but if ease of getting started is more important then it's a good choice. Their site has enough tutorials to see how things work and how you can make your first game. And I've written an implementation for the NewGrounds API in GDevelop so if you upload your game here you can add medals and leaderboards. One thing I'll say is that it will blithely allow you to pile all of your game's code into an Events page instead of breaking your code up into more modular pieces, and if you do that then you'll probably decide that your first game is done when the Events page gets to be such a complicated mess that you get afraid that if you make any more changes to it then it'll break the game. That's perfectly fine for your first game and not something you need to worry about just yet, but if you stick with GDevelop and make your second game in it, you'll want to learn how to make your own Functions and Behaviors to keep code neatly tucked away and avoid having a ginormous Events page. It will also be necessary if/when you decide to graduate from GDevelop and go to another engine.


Godot Engine (not to be confused with GDevelop even though they both start with G). This is my personal favorite and good for someone with some programming experience. It's suited for anything from a simple point-n-click to a complex 3D game. Some people think Godot doesn't handle 3D on the web and that Unity is your only option, but that's not true and I've published games that prove it. And it's much less bloated than Unity both in terms of file sizes on your computer and download times for the games you build. The documentation for Godot isn't as extensive as it is for Unity, so while I would recommend going ahead and starting with Godot and going through its docs to learn, it wouldn't be wrong to take the path I took of first learning in Unity and later switching to Godot. I would recommend working with GDScript (essentially Python) instead of C# because it seems to work better, and that's coming from someone who learned C# in Unity and never coded in Python before picking up Godot. Currently there are some minor issues with web builds but I've posted how to fix them: audio might play glitchy on Chromium browsers unless you route it through Howler.js, and if players change their browser zoom then it could jack up your graphics unless you do this.


Unity. The behemoth of game engines (excluding Unreal Engine, which I wouldn't recommend for web games because a nearly empty game ended up being almost 100 MB!). It will essentially let you make anything of any degree of complexity. It might be intimidating just because there's so much and it's unclear where to start, so to let you in on my path, I found the Ruby RPG tutorial and it's the single most useful tutorial I ever did. It pretty much taught me everything I needed to know to make my first game, and the fundamental game design concepts that let me dive into other game engines fairly easily. But be forewarned that I went into it already knowing how to program in a few languages including C so picking up C# was relatively easy for me; if you don't have programming experience then you'll probably have a rougher time with it. Downsides of Unity are its bloat which Godot is better about avoiding, C# probably being harder for most people new to programming than Python, and its lack of a good way of saving progress for web games. The easy way to save data is with PlayerPrefs, but that runs into the problem that any time you post an update to your game on a hosting website, the players will lose all of their save data. So if you use PlayerPrefs for saving, check out my post about how to make it not lose everyone's save data when you update. Or use a different and more flexible save handler that I wrote (which also makes it possible for multiple game devs to collaborate on a project with multiple cross-interacting games even if they're made in different engines, which I've done a proof-of-concept for), or write your own save handler based on this approach.


HaxeFlixel. This is a good framework to use if you want to do everything from code without the overhead of an IDE and the complexity of how things are done in Unity, and will let you make 2D games with pretty small file sizes and quick download times. Something that might be considered a downside is that there's less in terms of documentation than with the previous engines, but that might be a good thing: the tutorial (singular) and cheat sheet documentation that they do have is succinct and enough to learn everything important, so it will be a faster route to getting your first game made than wading through the Godot or especially Unity docs. When you download it, be sure to follow their instructions on incorporating it in Visual Studio Code because that will make your life a lot easier. The one thing that irked me when I learned it is that it wasn't clear from the documentation how things got layered when they're added to scenes which messed up my tilemaps, so I wrote a quick description about how to make it do what you want.


Phaser. This 2D framework is the ultimate in terms of keeping you as close as possible to the underlying JavaScript that a browser actually uses when it runs a game with no overhead. Basically you'll be writing and running your own JavaScript and using add-ins that Phaser sets up to make it easier to do game stuff. Be aware that the documentation on Phaser's website aside from their one simple tutorial is IMO the least well put together of all the engines/frameworks, so that isn't where I would recommend starting. Life will be easier if you start by learning JavaScript (ideally along with some HTML and CSS) from the MDN tutorials, and then jump into Phaser. I didn't do that and I went into the month-long Phaser game jam knowing how to program in C and C# from the Unity tutorials but not JavaScript, and I finished my game but it was hard and I ended up with a single huge messy JavaScript file that formed the basis for that warning I gave in the GDevelop section. Also, the Phaser docs will instruct you to set up a local server to playtest your game. You can do that in a number of ways, but the most elegant that I've found so far is described here.


Also, one thing you should know regardless of what engine you use: Once you've uploaded your game or are playing it locally from a web browser, you should open your web browser's Developer Tools (from the browser's menu or by pressing Ctrl-Shift-I) and look around, especially in the Console panel. That's where any errors will be displayed, and if something's not working right then that's probably the best place to find clues to fix it. Learn how to write to the Console from whatever game engine you're using (in Unity Debug.Log() shows up there, in Godot print() will do it, in HaxeFlixel you can use trace() or their more sophisticated debugger, and with JavaScript / Phaser it's console.log()) so you can see how far your code gets before things mess up and whether the variables really are what you expect them to be as the game is running.


I'm not going over everything you need to know or that would be useful in getting started, so I'm also making a thread in the Game Development forum for people to spill their thoughts on game devving. Especially when it comes to the actual design aspects -- what makes for a good top-down shooter, or a good RPG, or a good puzzle game design -- since I really focused on programming and getting started with an engine instead of how to design your game.


15