
[title] Publishing your game
[path] Creating and publishing your game/

In this guide you will learn how you can publish your game in the AirConsole store.

### *1. Add game in the developers console*

1. Go to the developers console: [airconsole.com/developers ](http://www.airconsole.com/developers)and login with your Google account.
2. Click on the "*Create new game"&#x20;*&#x62;utton.
3. Fill out the form, including uploading a cover art.
4. Click on "*Create game*".

:::BlockQuote
The game-id can't be changed later on!
:::

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/RF6sjA1A2ggCrtlSqbGel_image.png)

### *2. Zip your game files*

Create a .zip file which contains your game files.

:::BlockQuote
The screen.html and the controller.html have to be in the root of the .zip file.
:::

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/bUr6-85r4qdygU0Qn330i_image.png)

### *3. Upload your game*

**After** filling out the form and creating your game in the developers console you can upload your zip file.

Click on 'Choose File', select your zip and click on the 'Update' button.

Your game will now be uploaded.

:::BlockQuote
Don't worry, games which are not done are not going to be released. So you can also just upload a test-version to see how it performs
:::

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/s9y0MmJmzHKnBXMpnVeoq_image.png)

### *4. Versions and Review*

As soon as you have uploaded your zip file, you can use the preview link to test your game. Once you are sure your game works as intended, you can submit it for review. The AirConsole team will then look at and test your game and will directly contact you with feedback about what is left to do for it to be launched on AirConsole.

**AirConsole will not launch your games without your agreement.**

Whenever you upload a new zip file for your game, the old version won't get overwritten, but a new version will be added. Consequently you can rollback to old versions if something goes wrong.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/o9ulcVnRLibUVMvpZ1uoD_image.png)


[title] Technical Requirements
[path] Creating and publishing your game/

Please make sure your game meets these technical guidelines and [passes the requirements provided by the Checklist](https://developers2.airconsole.com/airconsole-checklist) before submitting it to AirConsole.
[The AirConsole best practices guide](https://developers2.airconsole.com/best-practices) provides information on how to make your game a success.
For Unity based games, [the Unity games on Android TV guide](https://developers2.airconsole.com/unity-game-on-android-tv) provides valuable guidance.

### *Runtime Environments*

- Games need to be able to run in modern browsers (ones with WebGL support), Unity WebGL exports are fine.
- Games need to be able to run on AndroidTVs with API level 34 (Android 14) and OpenGL ES2.
  - To ensure AndroidTV requirements are met, we recommend you to test on [the Mi TV Stick ](https://www.mi.com/us/product/mi-tv-stick/specs)or [the MiBox S](https://www.mi.com/global/product/mi-box-s/).

### *Game Sizes*

- Initial Game loads should not be more than 50MB until the first user interactions happen (e.g. Menu).
- Additional data loaded at a later point (e.g. a level) should not be greater than 50MB per loading break.
- Total required storage size on AndroidTVs should not exceed 500MB at a time (You may load data on demand from our servers directly into memory).
- Your AndroidTV game must be able to be deployed to Google PlayStore (AAB) and APK / APK+OBB based platforms like Amazon FireTV.
  - We will provide you with the projects for the different stores to upload your build to as part of the Android TV release cycle.

### *Performance & Memory constraints*

- Games should not require more than 512MB of RAM Memory (E.g. Unity WebGL memory size, [UnityEngine.Profiling.ProfilerRecorder](https://docs.unity3d.com/ScriptReference/Unity.Profiling.ProfilerRecorder.StartNew.html), Android Process Size).
- Games need to be able to run
  - **at 25 FPS on AndroidTVs** (Mi TV stick as reference) - 1GB of RAM, ARM Cortex-A53 ARMv7 Quad-Core at 1.2 GHz. ARM Mali-450MP3 GPU.
    - You can use an [old Android Smartphone with similar specs ](https://www.notebookcheck.net/ARM-Mali-450-MP4.116281.0.html)for a first feasibility tests, but should quickly move to Android TV development.
  - **at 30 FPS on modern chromium based webbrowsers&#x20;**&#x6F;n a computer with 4GB of RAM, 2GHz quad-core processor with an integrated graphics card.
- Game initialization after loading the data should be within a few seconds and always visualize that work is being done.

### *External Servers / CDNs / Storage*

- Depending on external servers or CDNs are not permitted if you manage them yourselves. Only big and managed Saas services (e.g. Photon) are permitted.
- Persistent Data of users (e.g. Save Files) need to use the AirConsole Persistent Storage API.

:::BlockQuote
If you have questions about these requirements or how to make sure your game meets them, reach out to the AirConsole team!
:::


[title] High Scores & Ghost Mode
[path] AirConsole Games Features/Improving your game engage/

Starting with AirConsole API 1.5 we are supporting global high scores. This allows players to see how good they are compared to their friends and the rest of the world. This also allows you to implement ghost modes (replay's of previous game runs). An example of a global High Score implementation can be found in the game [AirShields](https://www.airconsole.com/play/classic-arcade/airshields).

### *Storing High Scores*

To store a high score of a player you can call <font color="#eb144c">`airconsole.storeHighScore(...)`</font>. This will persistently store the high score for a device on the AirConsole servers. All data is public, so make sure you do not include sensitive data.

In this call you can set how high the score was (the higher the better), which device achieved the high score in which level of your game and you can also attach some custom data. <font color="#eb144c">`airconsole.onHighScoreStored`</font> will be called if the score was higher or equal to the last high score of this device with the new high score data for this device.

If you call <font color="#eb144c">`airconsole.storeHighScore`</font> from the Screen, make sure you pass the UID of the controller that achieved the high score, so the high score is associated with the user and not the screen the user is playing on.

It takes roughly 10 minutes before the high score is globally synced. The high score for connected devices however is always up to date.

An example call to store a high score of 14 points for Level "Single Player" (version 1.0) would look like this:

:::BlockQuote
airconsole.storeHighScore("Single Player", "version-1.0", 14, controller.uid);
:::

### *Requesting High Scores*

To request high scores from players you can call <font color="#eb144c">`airconsole.requestHighScores(...)`</font>

This will invoke <font color="#eb144c">`airconsole.onHighScores`</font> with at least 8 high scores, including the high scores of the connected devices. You can specify which kind of ranks you want to receive. Possible values are any combination of <font color="#eb144c">`["world", "country", "region", "city", "friends", "partner"]`</font>.

An example response in onHighScores would look like this:

:::BlockQuote
\[
&#x20; \{
&#x20;   "data": \{
&#x20;     "time": 25,
&#x20;     "points": 14
&#x20;   },
&#x20;   "level\_name": "Single Player",
&#x20;   "level\_version": "version-1.0",
&#x20;   "location\_country\_code": "CH",
&#x20;   "location\_country\_name": "Switzerland",
&#x20;   "location\_region\_code": "zh",
&#x20;   "location\_region\_name": "Zurich",
&#x20;   "location\_city\_name": "Zurich City",
&#x20;   "nicknames": \["Mandrooooo"],
&#x20;   "ranks": \{
&#x20;     "world": 7,
&#x20;     "country": 3,
&#x20;     "city": 2,
&#x20;     "friends": 6
&#x20;   },
&#x20;   "relationship": "facebook",
&#x20;   "score": 14.004,
&#x20;   "score\_string": "14 points in 25s",
&#x20;   "share\_image": "https\://www\.airconsole.com/games/image/?id=com.airconsole.games.airshields\&highscore=com.airconsole.games.airshields--1.0-1cd34b9bd6a077678441e0e6db5272cb\&ts=1470666148949",
&#x20;   "share\_url": "https\://www\.airconsole.com/play/?id=com.airconsole.games.airshields\&highscore=com.airconsole.games.airshields--1.0-1cd34b9bd6a077678441e0e6db5272cb",
&#x20;   "timestamp": 1469645461000,
&#x20;   "uids": \["1cd34b9bd6a077678441e0e6db5272cb"]
&#x20; },
&#x20; ...
]
:::

<font color="#eb144c">`location_*`</font> fields are not guaranteed to be returned.

If you are displaying a global high score board, it is probably a good idea to call<font color="#eb144c">`airconsole.requestHighScores`</font>whenever onHighScoreStored gets called and the passed argument is not false:

:::BlockQuote
airconsole.onHighScoreStored = function(highscore) \{
&#x20; if (highscore) \{
&#x20;   airconsole.requestHighScores(...)
&#x20; }
}
:::

If you don't do this and just display the newly achieved high score, multiple people might have the same rank in your global high score board (e.g. the player just became World #1 and the previous World #1 should now be World #2).

### *Custom High Score Data*

When storing high scores you can also pass custom data. Custom High Score data can be used for many things. For example to store a replay of the game run and implement a ghost mode. Note that when high scores are returned, the last 10 data entries are returned for every high score so you can add some variety to your ghosts. The last entry is the data that corresponds to the achieved score.

Another use case for custom high score data is signing high scores to verify that they are not fake. This works especially well for Unity games, because they are much harder to disassemble than normal html5 games. An example how to sign a high score would be to take the UID of the device and concatenate it with the high score and a secret string and take the hash from the concatenated string and store it in the custom high score data. When retrieving high scores you can then again concatenate the UID, the score and the secret string and check if the hash is equal to the stored hash in the custom data. Of course this is not completely secure, because the secret string also needs to be stored in the client source code, but it makes it much harder for someone to fake a high score.

:::BlockQuote
Custom data can be tampered with. Make sure your game does not crash, if someone maliciously inserts fake custom data.
:::

### *High Scores Ranks*

:::BlockQuote
If you request multiple rank types, not all returned high scores will have all requested rank types set.
:::

For example, if you request rank types <font color="#eb144c">`["world", "country"]`</font>, there can be high scores returned that only has the <font color="#eb144c">`world`</font> rank set (if they are not from the same country). Some entries may only have the <font color="#eb144c">`country`</font> rank set, because they are not in the world high scores.

:::BlockQuote
Requested high scores can have a rank value of <font color="#eb144c">`null`</font>, if they are not ranked in that rank type!
:::

### *Rendering High Scores*

To render the high scores, you probably want to group them by rank type first, so you can have separate tables for <font color="#eb144c">`World`</font>, <font color="#eb144c">`Country`</font>, etc.

:::BlockQuote
If you are using Unity, make use of the [AirConsole/extras/HighScoreHelper.cs](https://github.com/AirConsole/airconsole-unity-plugin/blob/master/Assets/AirConsole/extras/HighScoreHelper.cs) class to group high scores into tables.
:::

The following code snippet does exactly that in javascript. Feel free to copy and paste it.

```javascript
/**
 * Converts a list of high scores into rank tables
 * @param {Array<AirConsole~HighScore>} highscores
 * @return {Object} - The returned object is a dictionary of the following format:
 *                    { 
 *                      "world": {
 *                        "title": "World",
 *                        "highscores": [ ... array of high scores ...]
 *                      },
 *                      "country": {
 *                        "title": "Switzerland",
 *                        "highscores": [ ... array of high scores ...]
 *                      },
 *                      ...
 *                    }
 */
function convertHighScoresToTables(highscores) {
  var tables = {};
  for (var i = 0; i < highscores.length; ++i) {
    var highscore = highscores[i];
    for (var rank_type in highscore["ranks"]) {
      if (!tables[rank_type]) {
        tables[rank_type] = {"highscores": []};
        if (rank_type == "friends") {
          tables[rank_type]["title"] = "Friends";
        } else if (rank_type == "world") {
          tables[rank_type]["title"] = "World";
        } else if (rank_type == "country" || rank_type == "region" || rank_type == "city") {
          tables[rank_type]["title"] = highscore["location_" + rank_type + "_name"];
        }
      }
      tables[rank_type]["highscores"].push(highscore);
    }
  }
  return tables;  
}

```

### *Sharing High Scores*

Each high score has a <font color="#eb144c">`share_url`</font> and a <font color="#eb144c">`share_image`</font> property. If you want to display the high score for example on the controller, you can just create an \<img> tag and set the source to the <font color="#eb144c">`share_image`</font> property.

If you want to implement facebook or twitter sharing of a high score you can use the <font color="#eb144c">`share_url`</font> property. If the <font color="#eb144c">`share_url`</font> is shared on a social network, an image of the High Score will be displayed. When someone clicks on the post, they will be redirected to your games landing page. Here is an example for the controller:

```javascript
function shareOnFacebook(highscore) {
    airconsole.openExternalUrl(
        "https://www.facebook.com/dialog/share?display=popup&" +
        "app_id=428783770605591&href=" +
        escape(highscore.share_url));
}
function shareOnTwitter(highscore) {
    airconsole.openExternalUrl(
        "http://aircn.sl/airshields/?url=" +
        escape(highscore.share_url) + "&redirect=" +
        escape("https://twitter.com/intent/tweet?text=" +
        "Try%20to%20beat%20my%20highscore%20in%20AirShields&" +
        "hashtags=airconsole,airshields&url="));
}
```

Please make sure you call the<font color="#eb144c">`shareOnFacebook`</font>and<font color="#eb144c">`shareOnTwitter`</font>in an "onclick" handler on the controller, else iOS devices do not open a new window. Also note that we are using the aircn.sl URL shortener service for twitter, because twitter doesn't like long URLs. You can just replace "airshields" with your game name.

### *Team High Scores*

The High Score API supports passing an array of controller UIDs to the <font color="#eb144c">`airconsole.storeHighScore(...)`</font> method.

:::hint{type="warning"}
We do not recommend to pass the UID of the screen to airconsole.storeHighScore(...) for team high scores. Screens don't have nicknames or profile pictures, the nicknames property, the share\_image and the share\_url wont be useful to you. Use an array of controller UIDs instead.
:::



### *Partner specific high scores*

:::BlockQuote
Available on API 1.9.0 or Unity Plugin 2.5.0 or higher
:::

When including "partner" in the ranks array of <font color="#eb144c">`airconsole.requestHighScores(...)`</font>, the received highscores will contain additional information:

- partner: The name of the partner of the partner highscore rank.
- ranks.partner: The rank for the partner

:::BlockQuote
&#x20;         \{
&#x20;           ...
&#x20;           **partner**: "BMW",
&#x20;           ranks: \{
&#x20;             ...
&#x20;             **partner**: 1
&#x20;             ...
&#x20;           },
&#x20;           ...
&#x20;         }
:::

All highscores automatically contain information on the partner when storing highscores using<font color="#eb144c">`airconsole.storeHighScore(...)`</font>, if the screen is running on a system that is supported for partner highscore.

### *High Scores in Development Mode / Production Mode*

You can play with the high score API in development mode. Your high scores will be stored separately from the high scores in production. High Score data in production is shared across all production game versions.

:::BlockQuote
If you want to use a prepopulated database of high score ranks for testing purposes, you can specify <font color="#eb144c">`com.airconsole.highscore.test`</font> as the game id in development mode.
:::

The prepopulated database includes ranks from various countries.

You can also prepopulate your development database using the [High Score generator](https://airconsole.github.io/airconsole-documentation/tools/highscore_generator.html).

### *Selecting the best Ghosts from a list of High Scores*

As mentioned in the Custom Data section, you can use the data parameter to implement Ghost modes.

Obviously it is more fun to play against Ghosts that you know (Friends) and that have about the same skill level.

:::BlockQuote
We recommend that you set the <font color="#eb144c">`top`</font> parameter to <font color="#eb144c">`0`</font> and the <font color="#eb144c">`ranks`</font> to <font color="#eb144c">`["world", "friends"]`</font> when you call <font color="#eb144c">`airconsole.requestHighScores(...)`</font>. Else you are competing with the worlds best players and your friends will not be included.
:::

:::BlockQuote
If you are using Unity, make use of the [AirConsole/extras/HighScoreHelper.cs](https://github.com/AirConsole/airconsole-unity-plugin/blob/master/Assets/AirConsole/extras/HighScoreHelper.cs) class to select the best ghosts.
:::

The following javascript code snippet selects the best ghosts for you:

```javascript
/**
 * Given a list of high scores, returns the best high scores for ghost mode.
 * Preference in order:
 *   - First Ghosts with similar scores.
 *   - Then sort them by Connected Players, Friends, City, Region, Country, World.
 * If multiple Ghosts have the same preference score, random ones are chosen.
 * @param {Array<AirConsole~HighScore>} highscores
 * @param {number} count - The numnber of ghosts you would like to get
 * @param {number|undefined} min_score_factor - Optional minimum score factor a ghost needs to have
 * compared to the best score of a connected player.
 * Default is 0.75
 * @param {number|undefined} min_score_factor - Optional maximum score factor a ghost can have
 * compared to the best score of a connected player.
 * Default is 1.5
 * @return {Array<AirConsole~HighScore>}
 */

function selectBestGhosts(highscores, count, min_score_factor, max_score_factor) {
  if (min_score_factor == undefined) {
    min_score_factor = 0.75;
  }


  if (max_score_factor == undefined) {
    max_score_factor = 1.5;
  }
  var my_score = null;
  var result = [];


  for (var i = 0; i < highscores.length; ++i) {
    var highscore = highscores[i];
    if (highscore["relationship"] == "requested") {
      if (my_score == null || my_score < highscore["score"]) {
        my_score = highscore["score"];
      }
    }
    result.push(highscore);
  }
  
  var preference = {
    "world": 1,
    "country": 2,
    "region": 3,
    "city": 4,
    "friends": 5,
    "similar_score": 10,
    "requested": 20
  };
  var random_uid = (Math.random() * 65536) | 0;
  var random_uid_char = (Math.random() * 32) | 0;


  function sortScore(highscore) {
    var sort_score = 0;
    for (var rank in highscore["ranks"]) {
      sort_score = Math.max(sort_score, preference[rank]);
    }
    if (my_score != null) {
      if (highscore["score"] > my_score * min_score_factor &&
        highscore["score"] < my_score * max_score_factor) {
        sort_score += preference["similar_score"];
      }
      if (highscore["relationship"] == "requested") {
        sort_score += preference["requested"];
      }
    }
    var uid = highscore["uids"][random_uid % highscore["uids"].length];
    sort_score += 1 / uid.charCodeAt(random_uid_char % uid.length);
    return sort_score
  }

  result.sort(function (a, b) {
    return sortScore(b) - sortScore(a);
  });
  return result.slice(0, Math.min(result.length, count));
}
```

### *Checklist before launch*

:::BlockQuote
High Score Responses: Please make sure that your game does not crash in the following cases
:::

- If a<font color="#eb144c">`rank`</font>is not a number but null:

:::BlockQuote
\{
&#x20; ...
&#x20; "ranks": \{
&#x20;   **"world": null**,
&#x20;   "country": 3
&#x20; },
&#x20; ...
}
:::

- If<font color="#eb144c">`location_country_code`</font>or other<font color="#eb144c">`location_*`</font>fields are not present:

:::BlockQuote
\{
&#x20; ...
&#x20; **~~"location\_country\_code": "US"~~**
&#x20; ...
}
:::

- If there is valid JSON data in the Custom Highscore Data that is malicious (not what you expect):

:::BlockQuote
\{
&#x20; ...
&#x20; "data": \{
&#x20;   **"one\_of\_your\_fields": "malicious data"**
&#x20; },
&#x20; ...
}
:::

- If there is no custom data present at all:

:::BlockQuote
\{
&#x20; ...
&#x20; **~~"data": "your data"~~**
&#x20; ...
}
:::


[title] Game Development
[path] AirConsole Games Features/


[title] Handling different views
[path] AirConsole Games Features/Main Screen & Controller/

## *Controller.html — It’s a website*

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/F_d37lDJ8dVdZRNP21Blh_image.png" size="60" width="260" height="240" position="center" showCaption="false"}

An AirConsole controller is not part of the AirConsole Unity or Construct2 plugin. It has to be seen as a normal website. If you have ever written a website in html, css and javascript then you basically already know how to create a controller for an AirConsole game. The only difference is that your website runs on a mobile device and you have to learn how to use the AirConsole [Javascript API](https://airconsole.github.io/airconsole-api/).

## *Use different views to improve the user experience*

We encourage developers to display different views or “states” instead of packing everything into one.

Silly Run Valley — Controller when game is loading:

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/vgTBWoN5OrVe8jVeKpUny_image.png" size="40" width="356" height="563" position="center" showCaption="false"}

For example, if a player starts your game and it is still loading on the screen, then show a “Game is loading …” text on the controller, instead of already showing in-game buttons or a game-menu. This way the player always knows thats going on and the game is not broken.

## *Switching between different views*

The easiest way to switch between different controller views, is simply to have a \<div>-container for each view and handle their visibility with javascript and css.

Let’s say we have 3 different views:

- Loading (Show some loading spinner)
- Menu (could have game options, menu, character selection)
- In-game (Show your in-game controller)

For each view we create a \<div> container with a unique **id** and a **class** “*view*”. The id helps us later to identify the view.

:::BlockQuote
// Controller html
\<div id="loading\_view" class="view">Loading ...\</div>
\<div id="menu\_view" class="view">Your menu\</div>
\<div id="ingame\_view" class="view">Controller ...\</div>
:::

The css *.view* class helps us to define a general style for all containers. In the beginning, every container is invisible:

:::BlockQuote
// CSS style
.view \{
display\:none;
}
:::

Now, when we open the controller.html we would see nothing, because every container is set to *display\:none*.

To handle views and show/hide them we will write some Javascript. The main procedure to show one view is:

- Hide all containers (By class .view)
- Show the container by id

:::BlockQuote
\<script type="text/javascript>var showView = function(id) \{
&#x20; var view = document.getElementById(id);
&#x20; var all\_views = document.querySelectorAll('view');  // Hide all containers
&#x20; for (var i=0; i\<all\_views.length; i++) \{
&#x20;   all\_views\[i].style.display = 'none';
&#x20; }
&#x20;&#x20;
&#x20; // Show container
&#x20; view\.style.display = 'block';
};// Show the view with the id 'loading\_view':
showView('loading\_view');// A button to switch to the ingame view
var my\_button = document.getElementById('go\_to\_game');
my\_button.addEventListener('touchstart', function() \{
&#x20; showView('ingame\_view');
});\</script>
:::

This is of course a really primitive way of handling controller views.

If you want to have a bit more power, I recommend you to use at least a library such as jQuery, handlebars or even AngularJS.

## *Let the screen switch the view*

Of course you also want to let the screen decide when a view should change on one or more devices. For example when the game has ended, we want to show a ‘Game-End-View’ on every controller.

To communicate between a screen and it’s controllers we use the *AirConsole.message* and *AirConsole.onMessage* methods.

:::BlockQuote
// On your screen (or corresponding Construct2 / Unity methods)
// Code to send a message to all controllers
airconsole.broadcast(\{
&#x20; view: 'game\_end\_view'
});// In the controller.html
airconsole.onMessage(device\_id, data) = function() \{
&#x20; // Check if the data, we sent, contains view
&#x20; if (data.view) \{
&#x20;   showView(data.view);
&#x20; }
});
:::

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/w4gLhZlVeeZbEXyD-XUue_image.png" size="80" width="618" height="163" position="center" showCaption="false"}

In Construct2 you would use the **Send data** or **Broadcast** **data** actions. But because you are limited to sending just a string in Construct2, you would have to send something like “*game\_end\_view*” and then check in the *onMessage()* method which string was sent and then call the showView accordingly.

If you want to know more about what we have learned about making controllers for AirConsole, then check out the post: [AirConsole Dev Diary: Smartphones as Controllers](https://developers2.airconsole.com/smartphones-as-controllers).

[title] UX design for connect/disconnect events
[path] AirConsole Games Features/Connect to your game/

During the gameplay, players can connect, disconnect or reconnect to the games. As our catalog contains numerous multiplayer games and AirConsole has a focus around this, it's very important that each game is able to handle these players the best way possible.

For that reason, we have ranked on the following list where we included the 5 levels, going from the best to the worst, of how a game can handle players connecting and disconnecting to the games:

**Level 1:&#xA0;**

Game allows players to seamlessly drop in & drop out with no effect on players or gameplay.

Game examples: *PinWorld, RopeRaid, Trivia Crack.*

**Level 2:&#xA0;**

AI replaces dropped players or new players replace AI players.

Game examples: *GoKartGo, Ludo.*

**Level 3:**

Game prompt asking how to manage a connect/disconnect issue (wait for reconnect, continue game, go back to main menu), adding bots if needed.

Game examples:*&#x20;Starlit Kart Racing, Overcooked.*

**Level 4:&#xA0;**

No game prompt & game continues; disconnected player has a ‘wait’ message on controller device.

Game examples: *8bit Fiesta.*

**Level 5:&#xA0;**

No game prompt & game goes back to the main menu for all players.

Game examples: *Snowboard Buddies, Smoots Summer Games.*


**Additional notes:**

If the game is full, the player above the maximum should has a message indicating it.

If players disconnect from the session then a player that was ‘’waiting to connect’’  should automatically be placed in the lobby.

[title] Unity Game on Android TV
[path] Games in Unity/

An AirConsole app for AndroidTV is available on the [Google Play store](https://play.google.com/store/apps/details?id=com.airconsole.androidtv\&hl=en). Unfortunately, the AndroidTV cannot run the WebGL versions of Unity games that we have in the AirConsole store. In order to run your Unity-made AirConsole games on Android TV, you need to create a native Android Build instead of the regular WebGL build. Some adjustments may have to be made to your game project, as detailed in this guide.

When you switch platforms to Android and export your game to an AndroidTV, the AirConsole Plugin creates a webview that shows AirConsole content within your AndroidTV App.

Following this guide will give you an APK file which can then be uploaded to the Google Play Store. If you are thinking about porting your AirConsole game to AndroidTV, please contact us beforehand.

We have additional Unity version specific information at the end of this guide. Make sure to read it to avoid problems with your build process.

### *Tools*

- You want to make sure your Unity Version is the [latest Unity LTS version](https://unity.com/releases/editor/qa/lts-releases?major_version=\&minor_version=\&version=\&page=1) suitable for your game to be compatible with AndroidTV requirements:
  - 6000.0
  - 2022.3
- Make sure your AirConsole Plugin version is 2.5.7 or higher.
- Install the Unity SDK and NDK that can be installed as part of the Android Module in Unity Hub.

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/BHEZwIjR4HC9pxMCOx3VO_image.png" size="90" width="700" height="374" position="center" showCaption="false"}

### *Platform*

Before you start working on your AndroidTV Port, it's important that you have uploaded a .zip of your WebGL build to [airconsole.com/developers](http://airconsole.com/developers) and create the Game ID which we will need for the next steps. Read the ["Publish your game"](https://developers2.airconsole.com/publishing-your-game) guide which explains how to upload your game.

In **File**→**Build Settings**, choose 'Android'. Reimporting assets for the platform might take a moment.


::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/GltBS_vwKGbrnzA8alZW5_image.png" size="36" width="267" height="331" position="center" showCaption="false"}

### *BundleID*

Go to **Edit**→**Project Settings**→**Player**.

In the Other Settings tab, you'll find the parameter Bundle Identifier. For your AndroidTV build to work, this needs to be identical with the Game ID that you've given your game on [airconsole.com/developers](http://airconsole.com/developers). *N.B: It can also be called "Package Name" on some unity versions.*

:::BlockQuote
**ATTENTION:&#xA;**&#x54;he Bundle Identifier can not include com.airconsole.\*.
:::

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/xFVlBJ36-in8kFv6-YOSI_image.png" size="60" width="441" height="110" position="center" showCaption="false"}

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/acTid1Yoe_22sBtPaJJpU_image.png" size="80" width="669" height="42" position="center" showCaption="false"}

### *Configuring the Android platform*

Starting with Unity LTS 2021, Unity no longer includes OpenGL ES2 in builds with **Auto Graphics API** enabled in the Android Player Settings. You need to disable Auto Graphics API and manually add it back.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/8rHlLwtPqHnA703NVuEJq_image.png)

### *Screen and Controller Data*

In order for your exported game to be able to access the controller and all necessary JavaScript and image files, you need to:

- Have a WebGL build with all needed screen and controller Data uploaded to [airconsole.com/developers](http://airconsole.com/developers)
- Copy the Game upload version number of your game.

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/pRgUPFMIEUOmXQRR_uRJx_image.png" size="60" width="425" height="37" position="center" showCaption="false"}

- Paste the number into the **Android TV Game Version&#x20;**&#x66;ield on the AirConsole GameObject in your Scene.

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/aCqhv09pqATYuBf3EbxMA_image.png" size="88" width="691" height="188" position="center" showCaption="false"}

- Save your scene and create an Android build. The **gameName.apk&#x20;**&#x66;ile you get can be deployed to your AndroidTV device.

### *OnReady / OnGameEnd*

Users can return to the AirConsole store by selecting 'Change Game' on their phone without leaving your Android App. Because of that, you need to mute all sound in your game in the OnGameEnd event, like you would in the [OnAdShow](https://developers2.airconsole.com/airconsole-hero-and-revenue) event. Pause / stop as much of your game as you can to make sure no unnecessary computation is happening behind the web view after users have chosen to 'Change Game'.

Similarly, only start playing your game's sound/music in the OnReady event, not on Start / Awake / Scene Loaded.

### *Loading Screen Sprite*

You can choose a custom loading screen logo to be displayed when your game starts on Android TV. If there is none, a default AirConsole logo will be displayed.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/iu_gwo2_875lIfeM-FVu2_image.png)

### *UI*

On AndroidTV, the AirConsole Default UI bar will be laid over the game.The Default UI shows the AirConsole Logo, the session code and connected players' portraits and connection statuses.

To make sure your UI is displayed correctly despite the cutoff at the top, the AirConsole Object offers three options on how to deal with Canvases.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/OWdSkygf-__nn_Re9u831_image.png)

- **No Resizing**
  Your UI is left as it is, but the Default UI is laid over. If you choose this option you can either:
  - Manually adjust your UI so that nothing important will be cut off at the top.

- **Resize Camera**
  The height of the default UI is subtracted from the MainCamera's pixelRect. On your Canvas, set **RenderMode&#x20;**&#x74;o **Screen Space - Camera**. Now, if your UI elements' RectTransforms are correctly anchored, they will not be cut off by the default UI. Resizing the Camera will change its aspect ratio.

- **Resize Camera and Reference Resolution**
- The camera is resized as described in option 2). In addition, the Reference Resolution of your Canvas Scalers is adjusted to keep the aspect ratio it had before. This will ensure that UI elements are placed correctly in relation to the 3D elements in your game scene. It means, however, that your canvas is narrower than your Camera.

:::BlockQuote
**Execution Order:&#x20;**&#x54;he resizing happens in the OnLevelWasLoaded event. Do not depend on the order of OnLevelWasLoaded being before or after the OnEnable/Awake/Start Events.
:::

:::BlockQuote
**Nested Canvases:&#x20;**&#x57;e recommend not using nested canvases (i.e. a canvas as child of another canvas). Using nested canvases can lead to issues with some of the resizing options.
:::

### *Performance*

Even if your game is already well-optimized for Web, you may run into performance issues on AndroidTV. To test and improve performance, we recommend implementing an FPS counter in your game and testing on the targeted hardware frequently.

:::BlockQuote
**Testing:** If you do not own an Android TV device for testing, we recommend testing on an android phone instead. Once you have a working build, send it to us and we will test it on our Android TV devices. This is especially useful for testing UI adjustments and camera resizing.
:::

If your games performance is insufficient and [the Unity mobile optimisation tips](https://blog.unity.com/games/optimize-your-mobile-game-performance-tips-on-profiling-memory-and-code-architecture-from) were insufficient for Android TV, we have the following additional advise:

- Use multiple quality levels for your game driven by [SystemInfo.GraphicsDeviceType](https://docs.unity3d.com/ScriptReference/Rendering.GraphicsDeviceType.html):

  - A low quality for hardware that is used when the device only supports OpenGL ES2. Limit the maximum shader quality level / use low end mobile optimized approximations of the shaders, disable shadows, limit pixel lights to 1 and Anti-Aliasing to None or MSAAx2, limited or no anisotropic texture filtering.
  - A higher quality level based on OpenGL ES3 or Vulkan that can use more pixel lights, some amount of shadows, MSAAx4 and AA filtered textures.

- The hardware can support a maximum of 400 pure static batches / draw calls.

- Ensure that all textures are compressed to reduce load time and memory usage while improving performance. Optimally you would want ETC1 or distinct assetbundle/addressable levels for low end Android TV devices and the other platforms.

- Disable post-processing and visual effects on your camera if you detect that you are on a low performance device.

- Reduce transparent geometry, avoid using big, transparent sprites or dense particle effects.

- Reducing the resolution of your game with [Screen.SetResolution() ](https://docs.unity3d.com/ScriptReference/Screen.SetResolution.html)can be a solution to performance issues. If you do resize your game, call SetResolution in a Start event, not in Awake.

- Avoid using the Unity Scriptable RenderPipeline (URP, LWRP): It is not optimized for OpenGL ES2 creating significant performance problems.&#x20;

- Make use of the [UnityEngine.Profiling.ProfilerRecorder](https://docs.unity3d.com/ScriptReference/Unity.Profiling.ProfilerRecorder.StartNew.html) with [Memory Markers](https://docs.unity3d.com/Manual/ProfilerMemory.html#markers.html) and [Rendering Markers](https://docs.unity3d.com/Manual/ProfilerRendering.html#markers.html) to identify runtime performance in release build.
  \- If you need more detail, make development builds with the Autoconnect Profiler option enabled and see what has impact on performance

We recommend trying out one of these optimizations at a time and see which have impact, if necessary.

## *Release Build*

Once your game is ready for release, there are a few more things to consider:

**Debug Build**

Make sure the build you send is **not** a <font color="#eb144c">`DEBUG`</font> build. Make sure to uncheck the <font color="#eb144c">`Development Build`</font> checkbox in your Unity Build settings.

**Key & Key Store**

You need to sign your APK for release. To do so, you can create a new keystore in the Player Settings.

**Version**

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/MKF6lP29L4odhmrREKV63_image.png" size="80" width="1028" height="102" position="center" showCaption="false"}

Versions are used to determine the version of the build. This versions is very important on the Android store listings and for our QA to review and activate the respective game.
Please always increase the version if you create a new version for review.

**Bundle Code**

The Bundle Code is an integer that is used to determine whether one version is more recent than another, with higher numbers indicating more recent versions. You must increase the number by one every time you release a new version - the same bundle code cannot be used twice.

:::BlockQuote
**Note:** A bundle number may count as 'used' even when the build was not actually released publicly. To be on the safe side, increase your bundle code every time you send us a new build during the release process.
:::

**Android Manifest**

The Android Manifest is an .xml file that provides essential information about your app to the Android system, which the system must have before it can run any of the app's code.

In the manifest, you also determine which devices your game supports. The default AirConsole Android Manifest that we include in our plugin assumes you want your game to appear only on Android TV systems (which use the “leanback” Android UI) and not on Android Tablets.

If you want your AirConsole game to be playable on Android Tablets as well, simply remove the following lines from the Android Manifest:

:::BlockQuote
\<uses-feature
android\:name="android.software.leanback"
android\:required="true" />

\<category android\:name="android.intent.category.LEANBACK\_LAUNCHER" />
:::

**Screen Orientation**

On some devices, the screen orientation needs to be set explicitly. Make sure Screen Orientation is set to "landscape" in your Android Manifest: <font color="#eb144c">`android:screenOrientation="landscape"`</font>. [More information here](https://developer.android.com/guide/topics/manifest/activity-element#screen)

**Target API**

To support a wide range of devices, the minimum API Level has to be set down to <font color="#eb144c">`API Level 22`</font> and the target API Level to <font color="#eb144c">`API Level 33`</font> (at least).

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/4t4m6I2G8SL1u81EI-mrF_image.png)

**Build for Android 13 with Unity**

To support Android 13 (API level 34), the package visibility element was added to the AirConsole Unity plugin (v2.6 or later).

**Unity version specific configuration gradle configurations**

Instructions for supported versions of Unity are listed in the following sections. Versions of Unity that do not support customization of Gradle are unable to target Android 13 (API level 34) (Target API level 34) directly from a Unity build.

**Unity 2021 and newer**

Go to **Project Settings > Player > Android tab > Publishing Settings > Build**, and select both:

<font color="#eb144c">`Custom Main Gradle Template`</font>

<font color="#eb144c">`Custom Launcher Gradle Template`</font>

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/Nw67OpUrM0jsby0cE-37X_image.png" size="70" width="654" height="322" position="center" showCaption="false"}

Apply the following changes to both generated files:

<font color="#eb144c">`Assets/Plugins/Android/mainTemplate.gradle`</font>

<font color="#eb144c">`Assets/Plugins/Android/launcherTemplate.gradle`</font>

If present because the project was upgraded from an older Unity version, remove any block similar to the following commonly at the beginning of the file:

Please note: The AirConsole SDK Version 2.11 and before also provided these in its template, so when upgrading the AirConsole Unity SDK you need to remove these blocks from the templates.

:::BlockQuote
&#x20;     buildscript \{
&#x20;         repositories \{
&#x20;             google()
&#x20;             jcenter()
&#x20;         }
&#x20;         dependencies \{
&#x20;             // Must be Android Gradle Plugin 3.6.0 or later. For a list of
&#x20;             // compatible Gradle versions refer to:
&#x20;             // https\://developer.android.com/studio/releases/gradle-plugin
&#x20;             classpath 'com.android.tools.build\:gradle:3.6.0'
&#x20;         }
&#x20;     }

&#x20;     allprojects \{
&#x20;        repositories \{
&#x20;           google()
&#x20;           jcenter()
&#x20;           flatDir \{
&#x20;             dirs 'libs'
&#x20;           }
&#x20;        }
&#x20;     }
:::

Replace the following block if it is present:

:::BlockQuote
}\*\*PACKAGING\_OPTIONS\*\*\*\*SPLITS\*\*
:::

with the following block:

:::BlockQuote
\*\*PACKAGING\_OPTIONS\*\*\*\*PLAY\_ASSET\_PACKS\*\*\*\*SPLITS\*\*
:::

to enable Unity to work with Android AAB + PAD.

**Unity 2019.3 and 2019.4**

:::BlockQuote
**Note:&#x20;**&#x54;he following customization is supported only by **Unity 2019.3.7f1 or later**.
:::

Go to **Project Settings > Player > Android tab > Publishing Settings > Build**, and select both:

<font color="#eb144c">`Custom Main Gradle Template`</font>

<font color="#eb144c">`Custom Launcher Gradle Template`</font>

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/Fu8XI4xLihTrQaBiioO4z_image.png" size="80" width="654" height="322" position="center" showCaption="false"}

Apply the following changes to both generated files:

<font color="#eb144c">`Assets/Plugins/Android/mainTemplate.gradle`</font>

<font color="#eb144c">`Assets/Plugins/Android/launcherTemplate.gradle`</font>

If present, remove any block similar to the following which are leftover from Unity 2020 and before:
Please note: The AirConsole SDK Version 2.11 and before also provided these in its template, so when upgrading the AirConsole Unity SDK you need to remove these blocks from the templates.

:::BlockQuote
buildscript \{
repositories \{
&#x20;   google()
&#x20;   jcenter()
}
dependencies \{
&#x20;   // Must be Android Gradle Plugin 3.6.0 or later. For a list of
&#x20;   // compatible Gradle versions refer to:
&#x20;   // https\://developer.android.com/studio/releases/gradle-plugin
&#x20;   classpath 'com.android.tools.build\:gradle:3.6.0'
}
}

allprojects \{
repositories \{
&#x20; google()
&#x20; jcenter()
&#x20; flatDir \{
&#x20;   dirs 'libs'
&#x20; }
}
}
:::

Replace the following block:

:::BlockQuote
}\*\*PACKAGING\_OPTIONS\*\*\*\*SPLITS\*\*
:::

with the following block:

:::BlockQuote
\*\*PACKAGING\_OPTIONS\*\*\*\*PLAY\_ASSET\_PACKS\*\*\*\*SPLITS\*\*
:::

to enable Unity to work with Android AAB + PAD.

**Unity 2018.3 or earlier, including 2017**

Gradle customizations are not supported on these versions of Unity and are not compatible with the necessary changes to support Android 13 (API level 34).

**Build deliverables**

In order to be launched in the **Google Play store**, we need an AAB file (As small as possible) The non-data part may not be bigger than 150MB. If your AAB is bigger than 150MB and Unity can not address it by using Play Asset Distribution, it will inform you.

For more information, see [Unity documentation on AAB and PAD support](https://docs.unity3d.com/Manual/play-asset-delivery.html).

In order to be launched in the **Amazon Appstore**, we need an APK file (As small as possible). APKs may not be bigger than 100MB. If your APK is bigger than 100MB, it needs to be split into an APK and an OBB file. To split it, simply check the<font color="#eb144c">`Split Application Binary`</font>checkbox in **Player Settings --> Publishing Settings**.

For more information, see [Unity documentation on OBB support](https://docs.unity3d.com/Manual/android-OBBsupport.html).

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/rZg45ZyLCKta3vbsdDosF_image.png" size="80" width="491" height="101" position="center" showCaption="false"}

AirConsole is working with several distribution partners and not all systems support OBB files. If your APK's size exceeds 100MB, send us both the big, un-split APK as well as the split APK and corresponding OBB file.

**Android Build System**

In order for video ads to be displayed correctly, the<font color="#eb144c">`hardwareAccelerated`</font>parameter in the Android Manifest needs to be set to true. For the WebView's PostBuildProcess script to be able to overwrite the Manifest, your Android Build System needs to be set to **Gradle&#x20;**(not to Internal). You can set the Android Build System in the Build Settings or the Android Publishing Settings.
If your game was built using the Internal Android Build System before, you may have to delete your project's Library folder to force an asset reimport.

:::BlockQuote
**Check your Android Manifest:** You can use [Apktool](https://ibotpeaches.github.io/Apktool/) to unpack your APK after building and double check your Android Manifest. If <font color="#eb144c">`hardwareAccelerated`</font> is set to false, we recommend closing your project, deleting your Library folder and making a new, clean Build using the **Gradle** Build System.
:::

When Unity asks you if you want to upgrade the Android SDK to 34, accept this and download the SDK.

**Known issues:**

**Android Studio error "Installed Build Tools revision 34.0.0 is corrupted"***or* **Android Studio error "Installed Build Tools revision 34.0.0 is corrupted"**

This error can be solved following the link: [https://stackoverflow.com/a/68430992](https://stackoverflow.com/a/68430992)

**Force SDK 34 as target version if you previously hardcoded it**

If for some reasons, you do not success in compiling to SDK 34 or in using the build tools version 34, you can edit <font color="#eb144c">`Assets/Plugins/Android/launcherTemplate.gradle`</font> and <font color="#eb144c">`Assets/Plugins/Android/mainTemplate.gradle`</font> and hardcode the values:

:::BlockQuote
compileSdkVersion 34
buildToolsVersion '34.0.0'
:::

## *Promo Graphics*

**Android TV Banner**

In your Unity Player Settings, in the Icon tab of the Android export settings, check the<font color="#eb144c">`'Enable Android Banner'`</font>checkbox and assign a cover art for your game. This will be the icon that users see when they download your game onto their Android TV.

Please use the TV banner graphic preset provided [here](https://drive.google.com/drive/folders/0BxEs-y1NpJ4zOWpZbnktMnAta3M?usp=sharing) to adorn your banner with the AirConsole logo.

**Google Play visuals**

Along with your APK, please provide the AirConsole team with the following promotional graphics:

- **High-Res Icon&#x20;**- Resolution: 512 x 512, format: 32-bit PNG (With transparency).
- **Feature Graphic&#x20;**- Resolution: 1024 x 500, format: JPG or 24-bit PNG (Without transparency).
- **TV Banner&#x20;**- Resolution: 1280 x 720, format: JPG or 24-bit PNG (no alpha).
- **Ingame Screenshots x3** - Resolution: 1920 x 1080, format: JPG or PNG.

**Amazon Appstore visuals**

Along with your APK, please provide the AirConsole team with the following promotional graphics:

- **App icon&#x20;**- Resolution: 1280 x 720, format: PNG (Without transparency).
- **Background image&#x20;**- Resolution: 1920 x 1080, format: JPG or PNG (Without transparency).
- **Promotional image&#x20;**- Resolution: 1024 x 500 (Landscape only), format: JPG or PNG.
- **Feature content background image&#x20;**- Resolution: 1920 x 720, format: JPG or PNG (Without transparency). Some content might be cut, or overlayed with icons, please have a look at the asset guidelines of Amazon: https\://developer.amazon.com/docs/app-submission/asset-guidelines.html

## *Help needed?*

If you are a developer and are experiencing issues with updating or implementing our SDK, please reach out to us at [developers@airconsole.com](https://developers@airconsole.com) for assistance. We are here to help and will do our best to resolve any issues you may be facing as quickly as possible.

[title] Unity
[path] /


[title] Libraries
[path] Reference/

Extra useful information for developers:

- [https://assetstore.unity.com/packages/tools/game-toolkits/airconsole-42776](https://assetstore.unity.com/packages/tools/game-toolkits/airconsole-42776)
- [https://github.com/AirConsole/airconsole-leaderboard](https://github.com/AirConsole/airconsole-leaderboard)


[title] Construct3 Wiki
[path] Archive/


[title] Connect to AC Platform
[path] /


[title] AirConsole Checklist
[path] Creating and publishing your game/

On AirConsole we aim to provide high quality games. Before you submit your game you can use this checklist to make sure you have gone through every edge case.

Some topics and their solutions are covered in our [best practice guide](https://developers2.airconsole.com/best-practices).

## *General*

- [ ] My game includes the latest AirConsole API.
- [ ] All resources in my game are loaded over https.
- [ ] I have tested my game with unstable connection and higher latency. [(Read Guide)](https://developers2.airconsole.com/handling-unstable-connections)
- [ ] My game loads less than 50 MB (gzipped) of resources to start (enter Main Menu).
- [ ] Additional resources that are loaded later do not exceed 50 MB (gzipped) per request.
- [ ] I don't try to hide the top AirConsole bar (Using the DEPRECATED showDefaultUI() from the AirConsole API).
- [ ] If I use the Highscore API, I've tested that my game correctly handles all the cases from the [Checklist before launch ](https://developers.airconsole.com/high-scores-and-ghost-mode)list.

## *Screen*

- [ ] My game is self-explanatory or provides players with instructions on how to play.
- [ ] My game works on different screen resolutions / aspect ratios (Responsive design (TV resolution 960x540)).
- [ ] My game shows how many players are missing for the game to be started (Or: there is no minimal number of players in my game).
- [ ] If more players are connected than are allowed in my game, spare players are informed of that on their controllers (Or: there is no maximal number of players in my game).
- [ ] My game remains functional when devices connect during the game.
- [ ] My game remains functional when devices disconnect during the game.
- [ ] I did not hardcode device IDs and my game accounts for the fact that device ids may not be consecutive.
- [ ] My game displays the players' nicknames (see 'getNickname' in the API) instead of 'Player 1' etc. (Or: no names / titles are displayed).
-
- [ ] Menus (level selection, settings etc.) can only be controlled by the Master Controller.
- [ ] I have read and followed the [High Score Guide ](https://developers2.airconsole.com/high-scores-and-ghost-mode)and the launch checklist within (Or: my game does not use high scores).
- [ ] I am using the [AirConsole Keyboard ](https://github.com/AirConsole/airconsole-keyboard)for any text input in my game (Or: My game does not use any text input).
- [ ] I have added a [custom loading screen ](https://developers2.airconsole.com/unity-custom-loading)(Only games made with Unity).

## *Controller*

- [ ] My game works on different phone screen sizes (Min. 320x480).
- [ ] My game controller lets the player know which character / avatar they are controlling (using colors, labels etc).
- [ ] My game controller shows buttons labelled with their function or a relevant icon rather than letters that need to be explained on screen.
- [ ] The layout (buttons, images, text, options...) of my game controller adapts to different in-game situations (menu, in-game, game over etc.).
- [ ] The individual buttons on my game controller are as big as possible on the phone screen.
- [ ] My controller uses custom visuals that fit the art style of my game.
- [ ] I am not using any html \<img> tags on my controller, but set images as \<div> backgrounds instead (so that no 'save image as' option appears).

## *Saving player state*

- [ ] The player's game progress is saved using the controller's UID.
- [ ] Multiplayer progression is correctly saved in each player's UID on their controller.
- [ ] Sound and music settings are not saved but reset to default values on reload.
- [ ] Language settings are not saved but reset to the main screen language on reload.

## *Localization*

- [ ] I made sure that the game is fully playable in English. All text, menus, tutorials, instructions, and other information required to understand and play the game must be available in English. If additional languages are supported, each of those languages must likewise provide a complete and fully playable experience, with no required content left untranslated.
- [ ] I tested that the game loads the game in the same language as the main screen (browser or Android TV). If the game does not support the language of the screen, the game defaults to English. NOTE: Only the language of the screen is relevant to the game, not the language of the controllers.
- [ ] I tested that the phone loads the controller in the language set on the phone (defaulting to English if the language is unsupported). NOTE: The game and the controller may have different languages.
- [ ] I have tested that my custom language menu in the game selection is reset to the main screen language setting when the game is reloaded (meaning it's not saved into the player's state).

## *Screen browsers*

Your game works in all modern browsers.

- [ ] I have tested my game in the latest Chrome and Chrome 66 browser, and it works. I can use [Browserstack ](https://www.browserstack.com/)to ensure that everything works properly.
- [ ] I have tested my game in the Firefox browser and it works.
- [ ] I have tested my game in the Microsoft Edge browser and it works.
- [ ] I have tested my game in the Safari browser and it works.

## *Controller browsers*

Your game works in the AirConsole App as well as in all modern mobile browsers.

:::BlockQuote
If you are having trouble getting access to a specific operating system or browser, we recommend asking the AirConsole developer community on our [Discord server](https://discord.gg/3xtr7Rqf) for help.
:::

- [ ] I have tested my game on iOS in the Chrome browser, and it works.
- [ ] I have tested my game on iOS in the Safari browser, and it works.
- [ ] I have tested my game on iOS in the AirConsole App, and it works.
- [ ] I have tested my game on Android in the Chrome browser, and it works.
- [ ] I have tested my game on Android in the AirConsole App, and it works.

## *Hero Subscription Promotion*

Your game is showing ad breaks and handles them correctly. Remember that you can call showAd as often as possible and AirConsole will limit the actual ad frequency.

:::BlockQuote
See our [monetization guide](https://developers2.airconsole.com/airconsole-hero-and-revenue) for more information.
:::

- [ ] After each level (or 15 minutes) I call showAd in my game.
- [ ] My game is paused in the onAdShow event and all audio is muted.
- [ ] My game is resumed in the onAdComplete event and all audio is unmuted.
- [ ] I have tested that adding 3rd non-hero controller to the game shows a "Hero Subscription Promotion" ad.

## *Multi-screen games*

These apply only, if multi-screen multiplayer is implemented in the game. [(Read Guide)](https://developers2.airconsole.com/multi-screen-multiplayer)

Before submitting your game to review, please ensure that the game meets all the following requirements:

- [ ] With 2 or more screens connected in a game session, there is always only 1 primary screen.
- [ ] With 2 or more screens connected in a game session, there is always only 1 primary controller.
- [ ] With 2 or more screens connected in a game session, only the primary screen can control the game.
- [ ] With 2 or more screens connected in a game session, only the primary controller can control the game through the primary screen.
- [ ] With 2 or more screens connected in a game session, when the primary screen leaves the game session, only one of the secondary screens becomes the primary screen.
- [ ] With 2 or more screens connected in a game session, when the primary controller leave the game session, the new master controller on the primary screen becomes the primary controller.

## *Quality Control*

- [ ] I have verified that the game (screen and controller) immediately pause when they receive onPause / onAdShow (all game simulation, audio and video) and do not resume until onResume / onAdComplete is received.
- [ ] I have tested all single-player game modes, and the game runs smoothly without any bugs.
- [ ] I have tested all the multiplayer game modes, and the game works smoothly without any bugs.
- [ ] I have tested that all transitions between scenes in the game are instantaneous. If the loading screen is shown during the transition, the loading screen is not stuck (the loading screen has a continuous loading animation).
- [ ] If loading is longer than 30 seconds, I am showing to the user something visually dynamic on the screen.
- [ ] I have tested that when controllers leave or new ones join the game, it works gracefully - notifies the players, and the game state is not stuck. (The recommendation is not to allow to join in the middle of the level/race/gameplay but to take all the players to for example the main menu after showing a message on the main screen).
- [ ] I have tested that only the master controller can control the game menu.
- [ ] I have tested that when the master controller leaves the game, one of the remaining players is designated as the new master, and that controller can control the game menu.
- [ ] I have tested and confirmed that the game runs at 30 fps on modern Chromium-based web browsers. [(Read Guide)](https://developers2.airconsole.com/technical-requirements)

## *Launch & Marketing*

You have taken or are taking the necessary steps to launch and promote your game.

- [ ] I have prepared a text for my game's landing page (ca. 200-500 words).
- [ ] I have prepared a 30-second trailer for my game's landing page.
- [ ] I am promoting my game on social media and will keep posting about it on relevant groups and hashtags.

## *Android/Amazon TV*

See our [Android TV guide for Unity games](https://developers2.airconsole.com/unity-game-on-android-tv).

- [ ] The game uses the latest AirConsole plugin version.
- [ ] The APK/ABB is not in Debug mode.
- [ ] The game uses minimum API level 19 and target API level 34.
- [ ] I have tested and confirmed that the game runs at 25 fps on an Android TV device. [(Read Guide)](https://developers2.airconsole.com/technical-requirements)
- [ ] I have tested that the game audio is muted and the game is paused on the connect screen.
- [ ] I tested that when the last player presses the "leave session" on the controller, the game sound is muted, and the game shows the connect screen.
- [ ] I have tested that when I rejoin from the connect screen, the game starts from the main menu, and I can control the game without any problem.
- [ ] I created all Appstore visuals.

Once you have completed your launch checklist, please send all files as a ZIP file to developers\@n-dream.com.

[title] Reference
[path] /


[title] Unity Custom Loading
[path] Games in Unity/

The AirConsole Unity Plugin allows you to easily customize your Unity WebGL loading screen and loading bar.

:::BlockQuote
**Note**: Make sure your AirConsole Unity Plugin is updated to the [newest version ](https://github.com/AirConsole/airconsole-unity-plugin/tree/master)and that your Unity Editor is version 5.6 or higher.
:::

### *Loading Screen Background*

To replace the loading screen image, simply replace the png file found in <font color="#eb144c">`Assets/WebGLTemplates/AirConsole/TemplateData`</font>.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/uLoPWMDF9vW64dB4o3Veg_image.png)

### *Loading Bar customization*

You can customize the loading bar (Only works for the WebGL build, not AndroidTV) so that it matches your background image in size, placement and colors.

Once the game is fully loaded, both the bar and background disappear, revealing the loading screen image below.

**Unity 2019 and down**

Make sure that you selected the **AirConsole&#x20;**&#x57;ebGL template:


::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/SOrJvhoZma9nmk44tSfuI_image.png" size="60" width="300" height="111" position="center" showCaption="false"}

Go to your **index.html&#x20;**&#x74;hat can be found in<font color="#eb144c">`Assets/WebGLTemplates/AirConsole`</font>and update the<font color="#eb144c">`progress_bar_config`</font>object.

:::BlockQuote
function initAirConsole() \{
&#x20;var unity\_web\_config = \{
&#x20;  width: 16,
&#x20;  height: 9
&#x20;};
&#x20;var progress\_bar\_config = \{
&#x20;  left: "35%",
&#x20;  top: "65%",
&#x20;  width: "30%",
&#x20;  height: "5%",
&#x20;  background: "#000000",
&#x20;  color: "#ADEE00"
&#x20;};
&#x20;window\.app = new App('game', unity\_web\_config, progress\_bar\_config);
}
:::

- **left&#x20;**&#x61;nd **top**
  Determine where the top left corner of your loading bar will be placed.
- **width&#x20;**&#x61;nd **height**
  Determine how wide and tall the loading bar will be.
- **background**
  Determines the color of your “empty” loading bar. This color will gradually be hidden by the progressing bar drawn above it.
- **color**
  Determines the color of the actual progress bar. This color will gradually grow to match the loading progress.

**Unity 2020 and up**

For Unity versions 2020 and up the customization is simpler and you don't have to edit any HTML or JS files directly. Make sure that you selected the **AirConsole-2020&#x20;**&#x57;ebGL template in the **Player settings**.

Go to **Edit Project Settings > Player > Resolution and Presentation&#x20;**&#x61;nd edit the **Progress bar&#x20;**&#x66;ields.

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/pqeRDkOFz1LT3wC6cNwmL_image.png" size="80" width="400" height="218" position="center" showCaption="false"}

- Progress bar **background color.**
  Determines the color of your “empty” loading bar. This color will gradually be hidden by the progressing bar drawn above it.
- Progress bar **color.**
  Determines the color of the actual progress bar. This color will gradually grow to match the loading progress.
- Progress bar **height&#x20;**&#x61;nd **width.**
  Determine how wide and tall the loading bar will be.
- Progress bar offset **top&#x20;**&#x61;nd **left.**
  Determine where the top left corner of your loading bar will be placed.

The values for Progress bar **height/width&#x20;**&#x61;nd **top/left&#x20;**&#x61;re percentage based.

Make sure that you specify height/width **and&#x20;**&#x74;op/left values, otherwise it won't show the custom loading screen.

:::BlockQuote
**Note:** that upon exporting the game to WebGL, <font color="#eb144c">`index.html`</font> will be renamed to <font color="#eb144c">`screen.html`</font> by the Post Build Process.
:::


[title] API Methods Overview
[path] Getting started with AirConsole/

**message(device\_id, data) -> onMessage(device\_id, data)**

Send a message from the screen to a specific controller:

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/bxkFc6aAy40sL8YXvkZqU_image.png" size="50" width="462" height="220" position="center" darkWidth="462" darkHeight="220" showCaption="false"}

:::BlockQuote
The screen always has the device\_id = **0**. All other device\_ids should be seen as random.
We recommend to use the constant *AirConsole.SCREEN&#x20;*&#x69;nstead of 0
:::

Send a message from a controller to the screen:

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/dFCN14YsBSBy9vdc6nQ1S_image.png" size="70" width="462" height="261" position="center" darkWidth="462" darkHeight="261" showCaption="false"}

***broadcast(data) -> onMessage(device\_id, data)***

Send a message from the screen to all controllers:

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/h9Ys5zY24bzfiWqUDWVsA_image.png" size="70" width="462" height="220" position="center" darkWidth="462" darkHeight="220" showCaption="false"}

***setCustomDeviceStateProperty(key, value) -> onCustomDeviceStateChange(device\_id, data)***

A device sets it's custom data property and triggers the<font color="#eb144c">`onCustomDeviceStateChange()`</font>method on the other devices.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/OtBESg3w2VOU5Ks5yu2QP_image.png)

***setActivePlayers(max\_players)***

The screen sets maximum two devices as active players and triggers the<font color="#eb144c">`onActivePlayersChange()`</font>method on the first two devices.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/cbQTGHbUSBYZ8ys3kYM7p_image.png)


[title] Developer Revenue Share
[path] Generate income with your game/

### *Net Revenue Share for Developers*

**Example Revenue Share for one AirConsole Hero User**

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/wFKVyzcv4U4QkBAsmYFmj_image.png)

**Net Revenue Share for Developers**

- "Gross Revenue" is the USD price a user pays for AirConsole Hero per month (subscription cycle).
  The Gross Revenue is split into two parts:
  - "Transaction Fees" (30%): Used to pay the fees of the Google Play Store and the Apple AppStore.
  - "Net Revenue" (70%) is split into two parts:
    - "Net Game Revenue" (70%):
      - **You are entitled to a pro-rata share of the "Net Game Revenue" based on how many minutes the user spends in your game**
        (Compared to the total amount of minutes the user is connected to AirConsole in a subscription cycle).
      - If a user does not play your game in a subscription cycle, you shall not be entitled to a revenue share from that user.
      - Revenue shares are rounded to the nearest US Cent per user.
    - "AirConsole" (30%) is the share we use to pay the whole AirConsole Infrastructure, Development and Game Servers.
- In case a user buys an annual AirConsole Hero subscription, each subscribed year shall be treated like 12 consecutive monthly subscription cycles with a price of a 12th of the annual subscription price each.
- Developer revenue shares are calculated individually for every AirConsole Hero user.

If you have any questions regarding Developers Revenue Share, get in touch with our team ([developers@airconsole.com](mailto\:developers@airconsole.com)).

### *Terms and Conditions*

The AirConsole Terms and Conditions are based on Google's Terms and Conditions for Developers for the Google Play Store.

The AirConsole Developer Distribution Agreement can be found here:
[AirConsole-Developer-Distribution-Agreement.pdf](https://www.airconsole.com/file/AirConsole-Developer-Distribution-Agreement-2023-08-14.pdf).

The AirConsole Developer Content Policy can be found here:
[AirConsole-Developer-Content-Policy.pdf](https://www.airconsole.com/file/AirConsole-Developer-Content-Policy.pdf).

[title] Unity Game on Android Automotive
[path] Games in Unity/

# Introduction

With Unity AirConsole plugin 2.6.0, the plugin now supports Android Automotive AOSP based platforms. This platform is in addition to WebGL and Android TV based platforms.

To run safely in these environments, meeting their different requirements, significant work was done to support game developers to ensure consistent project configurations and provide native games with additional informations and features.

The most important feature addresses the situation, that some AirConsole Android target systems are displaying the game with a full screen resolution even if the system's user interface or the AirConsole WebView are overlapping.
Depending on the system's user interface state, games also have to handle resizing the available render area to make sure the game is displayed correctly within the available screen space.

Starting with 2.6.0 and initially for cars, we strive to make it consistent and ensure that game developers receive the required information to achieve the highest possible performance.

# Configuration

## Setting up the project

Setting up is relatively straight forward. How much work the integration is depends on degree of custom code your game uses.

1. Ensure that the project is using [Unity AirConsole Plugin v2.6.0 or newer.](https://github.com/AirConsole/airconsole-unity-plugin/releases)
2. On your AirConsle, enable **Native Game Sizing**

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/-Q8HdOWJquDWF_rk6sPgB_image.png" size="100" width="536" height="425" position="center" caption="AirConsole component" alt="Image showing how to enable native game sizing on the AirConsole component in Unity" showCaption="true" indent="2"}

3. Implement the ***AirConsole.instance.OnSafeAreaChanged*** on all cameras and camera setups in your game.
   In [Assets/AirConsole/examples/safe-area](https://github.com/AirConsole/airconsole-unity-plugin/tree/master/Assets/AirConsole/examples/safe-area), we provide examples for UI camera setups, multi-camera splitscreen and custom single camera setups.
   1. If you need to get the Safe Area independent of the event, you can access it through the property ***AirConsole.instance.SafeArea.***
4. Test that your integration is complete using the new **Safe Area Tester (Window > AirConsole > SafeArea Tester)**. This allows you to change the safe area while testing in editor playmode.

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/EmG-ubw7JtL3lHo-K6tBq_image.png" size="78" width="368" height="213" position="center" caption="SafeArea Tester" alt="SafeArea Tester unity editor window" showCaption="true" indent="2"}

5. If you use **Android UI Resize Mode**:
   *- Resize Camera* will automatically handle the safe area of the MainCamera (UnityEngine.Camera.main).
   *- Resize Camera and Reference Resolution* is not supported with **Native Game Sizing** active. To meet your specific needs in UI layout, you will need to implement this yourself. We provide an example for UI camera as a possible starting point.
6. Ensure that all your camera and UI canvas setups use **OnSafeAreaChanged**, including camera independent overlay canvas setups. For more information see **Usage** below.
7. Once you are done with the integration, create and upload a new Unity WebGL build to [https://airconsole.com/developers](https://airconsole.com/developers) and update the **Android Game Version** on the AirConsole component. to ensure that your screen will be configured correctly.

## Verifying your 3rd party dependencies

Android Automotive AOSP environments have several key differences to Android TV based environments that can have an impact on games built for the Google Play store. Please verify the following aspects thoroughly:

1. There are no Google APIs including Google Play, Google Analytics and other com.google based abilities. You need to remove them completely from your game or the game will be able to launch at all.
   1. This can be verified using an Android Automotive AOSP based Android Emulator in Android Studio for example.
2. Automotive manufacturers need to follow strict security and data protection regulations. Part of that is that applications are not able to contact whatever web address they want, there is a strict whitelist with everything else being blocked.
   1. As on all platforms, your game is not meant to use anything not provided by AirConsole, which use airconsole.com or related subdomains that are whitelisted.
      If you followed this and related [Release Checklist Requirements](https://developers.airconsole.com/airconsole-checklist), you are fine.
      If your game depends on 3rd party services not provided by AirConsole thus far, please verify beforehand with the AirConsole team, if you can continue to use that dependency or what can be done about it for your game to become available on automotive.
   2. A good example for this are multiplayer game servers. If you run them yourself, we can run them in our infrastructure.
   3. Games sent to the store platform potentially need to make legally binding declarations on security and vulnerability aspects.
      For Unity and the AirConsole plugins we do this, but for third party dependencies, we might need you to provide these.
      We understand from experience that this can be extremely complex, as such we recommend you to have only the native library dependencies and internet based dependencies absolutely necessary that are well enough understood.

# Usage

## OnSafeAreaChanged event

The `OnSafeAreaChanged` event is triggered by the AirConsole game platform when the system user interface and/or the AirConsole Webview overlaps the games render area. Everytime this event is called new offsets the new render width and height are provided.

![](https://archbee-image-uploads.s3.amazonaws.com/DlW7QKqnO54P6fJk_G6J2-ARQ7ME6_niZ1GthYa_MJu-20250610-113825.png)

**Note**: The safe area can change at any time while the UI and the rendered game must respect the new safe area received in the `OnSafeAreaChanged` event.

Examples for safe area handling can be found in the Unity plugin in [Assets/AirConsole/examples/safe-area](https://github.com/AirConsole/airconsole-unity-plugin/tree/master/Assets/AirConsole/examples/safe-area)

## Camera Handling

As the game is occupying the fullscreen and system overlays are not changing the screen size of the game we have to adjust the area the game renders in by setting the cameras viewport rect.

The implementation of the `OnSafeAreaChanged` event in the Unity AirConsole Plugin provides a value of type `Rect` that uses the bottom-left relative values that Unity uses for `Camera.pixelRect`.
This means for non-splitscreen games you can directly pass the value to the `pixelRect` property of the camera.

:::BlockQuote
private void HandleSafeAreaChanged(Rect newSafeArea) \{
&#x20;   if (targetCamera) \{
&#x20;       targetCamera.pixelRect = newSafeArea;
&#x20;   }
}
:::

This example expects a variable `targetCamera` that is containing a reference to the active camera that renders the game.

In the case of split screens, the safe area needs to be split up to size and scale the cameras. The plugin provides [Assets/AirConsole/examples/safe-area/SplitScreenSafeAreaHandler.cs](https://github.com/AirConsole/airconsole-unity-plugin/blob/master/Assets/AirConsole/examples/safe-area/SplitScreenSafeAreaHandler.cs)  as a starting point.

## UI and Canvas

Canvas components that are drawn on Screen Space need to adjust their visible pixelRect according to the safe area as well to avoid rendering UI outside the visible area. For an example to get started, you can check

[Assets/AirConsole/examples/safe-area/UISafeAreaHandler.cs](https://github.com/AirConsole/airconsole-unity-plugin/blob/master/Assets/AirConsole/examples/safe-area/UISafeAreaHandler.cs)

## Examples

Examples for common challenges like

- UI camera setups
- single camera setups with custom logic
- multi-camera splitscreen setups

can be found in the AirConsole Unity Plugin examples:
[Assets/AirConsole/examples/safe-area](https://github.com/AirConsole/airconsole-unity-plugin/tree/master/Assets/AirConsole/examples/safe-area)

# Limitations

1. Android projects that do not have a web version live yet require support from the AirConsole team once the first version has been uploaded, as Android games require at least one live version and a special flag.
2. As of **Unity Plugin 2.6.0 on Aug 18, 2025**, the safe area only works for supported automotive partners. Everywhere else it will behave like Android TV with the top bar.
   We plan to expand it's support to all android based environments including Android TV in a next step.


[title] Construct
[path] Archive/

In this guide you will learn how to use the Construct2 AirConsole plugin to add local multiplayer to your game.

:::BlockQuote
If you are new to AirConsole then you should read the [Getting Started Guide](https://developers2.airconsole.com/)
:::

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/YJ5ooYD5BGFOBsXCWKBv3_image.png)

Crystal Control - made with Construct2

### *Setup*

You can download the plugin files at our [airconsole-construct2](https://github.com/AirConsole/airconsole-construct2) github repository.

1. [Download](https://github.com/AirConsole/airconsole-construct2) the <font color="#eb144c">`airconsole.c2addon`</font> file on github.
2. Open Construct2.
3. Drag and drop the <font color="#eb144c">`airconsole.c2addon`</font> file into the Construct2 window.

### *Enable AirConsole*

To be able to use the AirConsole in a Construct2 game you have to add a new object type. You will find the AirConsole Plugin in the "Web" category.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/90vccMsUMDqd0hVmo4Q40_image.png)

### *Conditions*

If you create a new event in an event sheet, then you will find the following condition options:

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/zS8CSgAyJj5lm3649sY7m_image.png)

**Conditions reference:**

| Name                         | Description                                                                                                                                                                    |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| On device custom state check | Triggered when a device custom state matches a value (E.g. when calling airconsole.setCustomDeviceState(\{key: value}) on a controller).                                       |
| On message                   | Triggered when a device sends a message (\{message: "your-value"}) to the screen and the device id matches                                                                     |
| On specific message          | Triggered when a specific message is received from a specific device.                                                                                                          |
| On message from              | Triggered when any message is received from a specific device.                                                                                                                 |
| On message key               | Triggered when a device sends a message (\{key: "your-value"}) and the key matches *(Use this if the sender device id does not matter - e.g. for an action to start the game)* |
| On device join               | Triggered when a device joins                                                                                                                                                  |
| On device left               | Triggered when a specific device disconnects                                                                                                                                   |
| On any device left           | Triggered when any device disconnects.                                                                                                                                         |
| On too many players          | Triggered when max players is exceeded.                                                                                                                                        |
| OnPause                      | Triggered to pause the game. Pause should also pause any audio.                                                                                                                |
| onResume                     | Triggered to resume the game again.                                                                                                                                            |

### *Actions*

The following actions are available:

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/xoNi40kZZWpx5c0SzyEda_image.png)

**Actions reference:**

| Name                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Broadcast data          | Sends a message to all connected devices                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Send data               | Sends a message to a specified device id                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Game ready              | Sends a message to all devices that the game is ready (**Use this always on game start!**).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| Set custom device state | Sets the screen-device custom state (Same as calling airconsole.setCustomDeviceState(\{key: value}) in the API).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| Set active players      | Takes all currently connected controllers and assigns them a player number. Can only be called by the screen. The assigned player numbers always start with 0 and are consecutive. You can hardcode player numbers, but not device\_ids. Once the screen has called setActivePlayers you can get the device\_id of the first player by calling convertPlayerNumberToDeviceId(0), the device\_id of the second player by using ConvertPlayerNumberToDeviceId(1). You can also convert device\_ids to player numbers by using ConvertDeviceIdToPlayerNumber(device\_id). You can get all device\_ids that are active players by using GetActivePlayerDeviceIds(). |
| Show ad                 | Show ad on every connected controller and screen. onAdComplete is called when showing ad is over                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |

### *Expressions*

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/srSjkBrT7fGMFiTnV0GtF_image.png)

Update December 2022:

| Name           | Description                                                                                |
| -------------- | ------------------------------------------------------------------------------------------ |
| GetLanguage    | Gets the language ISO Code (E.g. 'en', 'es')                                               |
| GetTranslation | Gets the [translation](http://localhost:8080/#!/guides/translations) for a translation-key |

You can find more about expressions in [this tutorial](https://www.scirra.com/tutorials/9415/airconsole-little-expression-guide).

### *Properties*

| Max players      | Sets the maximum amount of players the game can handle. (All other players who join after that should get a message like "Game is full") |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Use translations | Set to True if your game uses translations and you want to load them                                                                     |

### *Method examples*

Look at some of the examples on how to use the methods.

**ControllerDeviceIds**

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/YIVgPCEOmJyExHHNLvzQG_image.png)

**MasterControllerDeviceID**

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/d1GDSeV5in6ie3tjVGl19_image.png)

**ActivePlayerDeviceId**

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/PWeGrMUyJCpCro1B7pCYs_image.png)

**Storage**

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/T6bW_8vvwvqPqCbjkcHVS_image.png)

### *Connect screen with controllers*

For a usual HTML5 game we would normally just wait for the screen.html to load and then handle the devices. With Construct2 we have to wait for the entire game to initialize, because we are "talking" with AirConsole in the Construct2 context.

**Construct2 game ready event - Asking "who is here?"**

Simply call the<font color="#eb144c">`AirConsole.GameReady`</font>action once as soon as the game has started. This will broadcast a message to every already connected controller with the data<font color="#eb144c">`{ handshake: true }`</font>.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/2zLORGOPdQIFZAvuH0YAS_image.png)

**Code in controller.html - Replying "me is here"**

The controller listens for the<font color="#eb144c">`{ handshake: true }`</font>data in the<font color="#eb144c">`onMessage`</font>method:

```html
<script type="text/javascript" src="https://www.airconsole.com/api/airconsole-1.9.0.js"></script>
<script type="text/javascript">
  var air_console = new AirConsole();

  var sendHandshake = function() {
    air_console.message(AirConsole.SCREEN, {
      handshake: true
    });
  };

  air_console.onReady = function() {
    sendHandshake();
  };

  // Let the screen know we are here
  air_console.onMessage = function(device_id, data) {
    if (data.handshake) {
      sendHandshake();
    }
  };
</script>
```

The controller will send a message back to the screen and trigger<font color="#eb144c">`AirConsole.onDeviceJoin`</font>events in Construct2, which you will also have to add. :)

### *Controller messages*

To send a message from a controller to the screen you have to stick to certain keys in the JSON data you send, so that the Construct2 plugin knows which condition you want to trigger.

| Key                                      | Type         | Description                                                             |
| ---------------------------------------- | ------------ | ----------------------------------------------------------------------- |
| <font color="#eb144c">`handshake`</font> | *\<Boolean>* | Send this as soon as the game is loaded and to register the controller. |
| <font color="#eb144c">`key`</font>       | *\<String>*  | Applied to the AirConsole.MessageKey expression                         |
| <font color="#eb144c">`message`</font>   | *\<String>*  | Applied to the AirConsole.Message expression                            |

**For example**

:::BlockQuote
&#x20;   \<script type="text/javascript" src="**https\://www\.airconsole.com/api/airconsole-1.9.0.js**">\</script>
&#x20;   \<script type="text/javascript">
&#x20;     var air\_console = new AirConsole();
&#x20;     // ...
&#x20;     air\_console.message(AirConsole.SCREEN, \{
&#x20;       **key**: "show\_main\_menu"
&#x20;     });

&#x20;     air\_console.message(AirConsole.SCREEN, \{
&#x20;       **message**: "move\_left"
&#x20;     });
&#x20;   \</script>
:::

### *Export your game*

Export the game as a usual HTML5 game. AirConsole will request a<font color="#eb144c">`/screen.html`</font>, which is why you should rename the<font color="#eb144c">`index.html`</font>into<font color="#eb144c">`screen.html`</font>. And of course, don't forget to create a<font color="#eb144c">`controller.html`</font>in the same directory, which includes the functionality for the controllers.

As soon as your game is ready make a zip file of the whole directory (screen.html and controller.html in the root) and [publish](https://developers2.airconsole.com/publishing-your-game) it on AirConsole.

### *Example Pong Game walktrough*

To show you how all of this works together we will go through our example (very basic) pong game.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/5ThcPRBZ9Qb4nfrQNbAo4_image.png)

[Play example](https://www.airconsole.com/simulator/#!play=com.demo.construct)

Download Game: [https://github.com/AirConsole/airconsole-construct2/tree/master/example\_project](https://github.com/AirConsole/airconsole-construct2/tree/master/example_project)

**Event sheet**

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/5iJRhPufAj9HEUJaCMK0i_image.png)

**Trigger the GameReady Event**

As mentioned before, you **ALWAYS&#x20;**&#x68;ave to trigger a Game ready event when the game has loaded, so that the controllers can tell us they are there.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/6JB7yHJzmcM6_hlsUIKVm_image.png)

**Store and assign device\_ids**

Every controller owns a paddle-sprite. Which means if controller-2 presses the up-button, then we want the right paddle to move up. That means we have to know which device belongs to which sprite. We can achieve this by creating global vars for each device the game will support (In pong two global vars, because we will just have two players in our game).

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/nrs3utCqLzHW6f5Mgplar_image.png" size="50" width="310" height="269" position="center" showCaption="false"}

If the first controller connects "On device join", then we assign the joined AirConsole.device\_id to the global DeviceID1\_Move. The second devices which connects get assigned to DeviceID2\_Move.

:::BlockQuote
**Device\_ids are random&#xA;**&#x54;he value of DeviceID1 does not have to be always "1". The device with another device\_id (2, 3, 6, ...) could join before the device\_id 1.
:::

**Device joins, set the device\_id**

With the condition<font color="#eb144c">`AirConsole.onDeviceJoin`</font>we can listen for events when a new device joins. If device joins, then assign the global variable device\_id to the value of<font color="#eb144c">`AirConsole.DeviceIDJoin`</font>.
The tricky part here is to have the order of devices which join in mind. This means the first player who joins should be assigned to DeviceID1. The second player should be the DeviceID2. Consequently we also have to check if the DeviceID1 already has a value. If yes, we check if the DeviceID2 var has none and if that is true we assign the device\_id.

**Move paddles**

To move our paddles we create another two global vars called<font color="#eb144c">`DeviceID1_Move`</font>and<font color="#eb144c">`DeviceID2_Move`</font>. Each of them can have one of the 3 values:

- -1 = Move up
- 0 = Stop
- 1 = Move down

**Send a message from a controller to the screen**

Our controller will only consist of two buttons: move up and move down. If we press one of this buttons we will send a message to the game (screen) and the corresponding paddle-sprite object should move - or if we release a button it should stop.

Our<font color="#eb144c">`controller.html`</font>contains the following code:

```html
<button id="up">MOVE UP</button>
  <button id="down">MOVE DOWN</button>

  <script type="text/javascript" src="https://www.airconsole.com/api/airconsole-1.9.0.js"></script>
  <script type="text/javascript" src="//code.jquery.com/jquery-3.1.1.min.js"></script>
  <script type="text/javascript">

    var airconsole = new AirConsole();

    // Let the screen know we are here
    var sendHandshake = function() {
      airconsole.message(AirConsole.SCREEN, {
        handshake: true
      });
    };

    airconsole.onReady = function() {
      sendHandshake();
    };

    airconsole.onMessage = function(device_id, data) {
      if (data.handshake) {
        sendHandshake();
      }
    };

    $("#up").on('touchstart', function () {
      airconsole.message(AirConsole.SCREEN, {
        message: 'up'
      });
    });

    $("#down").on('touchstart', function () {
      airconsole.message(AirConsole.SCREEN, {
        message: 'down'
      });
    });

    $("#up").on('touchend', function () {
      airconsole.message(AirConsole.SCREEN, {
        message: 'stop'
      });
    });

    $("#down").on('touchend', function () {
      airconsole.message(AirConsole.SCREEN, {
        message: 'stop'
      });
    });

  </script>
```

Now the game has to listen to a message which has the content "up", "down" or "stop".

In our event sheet, you can see two events of "On receive message data". They were added by the <font color="#eb144c">`AirConsole.onMessage`</font> event like this:

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/3wpOSrlwpIa5WHC1U4MNd_image.png" size="80" width="586" height="240" position="center" showCaption="false"}

To sum it up:
**IF&#x20;**&#x6D;essage == "up" AND device\_id matches the value of DeviceID1 **THEN&#x20;**&#x77;e set the variable<font color="#eb144c">`DeviceID1_Move`</font>to -1, which means our sprite moves down.

The same happens with message == 'stop' or message == 'down'.

### *External Tutorials*

- [HowTo: Construct2 and AirConsole](https://laurentchervet.wordpress.com/2016/11/30/howto-construct2-and-airconsole/)
- [Construct 2 and AirConsole](https://www.scirra.com/tutorials/9414/construct-2-and-airconsole)
- [Little expression guide](https://www.scirra.com/tutorials/9415/airconsole-little-expression-guide)

### *Troubleshooting*

**Game won't work because of Javascript errors**

If you get any javascript errors try to export your game without checking the "Minify script" checkbox.

[title] AirConsole Hero & Revenue
[path] Generate income with your game/

### *Monetization Basics*

AirConsole and its developers earn money through a paid service called [AirConsole Hero](https://www.airconsole.com/hero). Users can try a limited version of AirConsole for free, or pay for the full version.

In the free version, there are “breaks” where we ask the user to wait or show them the benefits of getting a subscription. There are no third party advertisements on AirConsole and therefore no ad revenue.

The exact features of the free and paid versions are subject to changes. We frequently and extensively A-B-test the exact offering in order to find the right balance between what our users like and what benefits the developers.

### *Revenue Share for Developers*

Every user’s subscription revenue is split among the games that the user played after the shares for App Stores and AirConsole Infrastructure have been deducted. See our exact legal terms [here](https://developers2.airconsole.com/revenue-page).

:::BlockQuote
**Fun Fact:** Statistically, every Hero User on AirConsole has a favorite game in which they spend about half their total playtime.
:::

This is an example of how gross revenue is distributed on AirConsole:

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/YGNUZv49Nbv0S52yuE1B9_image.png)

## *AirConsole Ad Break Implementation*

AirConsole does not show third party ads, but there are breaks in which we show advertisements for [AirConsole Hero](https://www.airconsole.com/hero).

When an AirConsole ad is shown, it will be displayed on all connected devices - the screen as well as on all controllers. AirConsole is taking care of everything concerning displaying the ads.

**Game developers must specifically demand to show an ad by calling an API method (see "API Methods" below).**

### *API Methods*

:::BlockQuote
You need the API version 1.4.0 or higher!
:::

**showAd()**

The<font color="#eb144c">`showAd()`</font>method requests that AirConsole shows a multiscreen advertisment. This method can only be called by the AirConsole screen.

**onAdShow()**

Calling<font color="#eb144c">`showAd()`</font>does not necessarily mean that an ad will be shown. However, if an ad is shown, the<font color="#eb144c">`onAdShow()`</font>method is called. You can use this method to pause your game and mute all sounds.

**onAdComplete(ad\_was\_shown)**

This method will be called if an ad was closed (whether it was shown or not). Use the<font color="#eb144c">`ad_was_shown`</font>param to find out if and ad was shown.

### *Javascript example*

:::BlockQuote
// Our airconsole instance
&#x20; var airconsole = new AirConsole();

&#x20; // The perfect moment has come to show an ad:
&#x20; airconsole.showAd();

&#x20; // Overwrite the onAdShow method
&#x20; airconsole.onAdShow = function() \{
&#x20;   // Pause the game and mute all sounds
&#x20;   game.pause();
&#x20;   game.sound.mute();
&#x20; };

&#x20; // Overwrite the onAdComplete mthod
&#x20; airconsole.onAdComplete = function(ad\_was\_shown) \{
&#x20;   // Unpause the game and play sounds again if ad was shown
&#x20;   if (ad\_was\_shown) \{
&#x20;     game.unpause();
&#x20;     game.sound.play('prodigy');
&#x20;   }
&#x20; };
:::

## Any Questions?

Head over to the [Help Page](https://developers2.airconsole.com/faq-help) for answers to frequently asked questions about AirConsole Hero, ads and revenue shares.

[title] Persisting Player Data
[path] AirConsole Games Features/Improving your game engage/

## *What is the Persistent Storage API?*

[The Persistent Storage API](https://airconsole.github.io/airconsole-api/AirConsole-Persistent_Data.html) enables your game to store data on behalf of your players to continue anywhere, anytime.

You can think of [the Persistent Storage API](https://airconsole.github.io/airconsole-api/AirConsole-Persistent_Data.html) as AirConsoles version of a **save game system that stores the data for players** but with additional possibilities towards unique game designs that go well beyond traditional savegame capabilities.

## *Before you get started*

There are different ways to implement persistent data storage for your game depending on your game design.

This gives you unique possibilities to use persistent data as a gameplay feature in your game design.

### *1. Which players need to store and request part of the game state*

- You can store and request data for every player in the game and selectively decide what to apply when and where (see Working with data from multiple connected players below).&#x20;
- You can store and request data for the player driving the session (see Persisting data only for the controlling player below).

:::BlockQuote
When working with the Persistent Storage API, keep in mind that the storage limit is **1MB per UID per game!**
:::

### *2. How do you want to use and persist the data in your game*

- Decide at what points you want to store the persistent data.

- Decide what happens when the owner of the persistent data leaves:

  \- Will future updates to the persistent data be stored for the leaving owner of the current game state?
  \- If not, will the players be able to continue but the general game state progress will no longer be persisteted, only player specific personal progress?
  \- Or do you use the flexible capabilities to offer a unique approach?

- Remember the players uids for which persistent data was requested for the lifetime of the usage of this game state to persist it for the correct player again.

## *Working with data from multiple connected players*

AirConsoles Persistent Storage API nature as key - value based store gives you unique flexibility in deciding which controller contributes what information to the game worlds state
This allows you to request and store data for multiple players in the same game and enable an optimal experience by combining the data in ways providing the best game experience.

**Example**

- Level unlocks are taken from the device with the most advanced state and to this device this part of the state is persisted.
  - **example key:&#x20;**&#x6C;evel\_progress
- But every player can have their own unlocks (characters, gadgets, skins, abilities, features, ...) which are stored based on their uid.
  - **example key:&#x20;**&#x70;layer\_progress

In this example, the game requests and stores the progress of the world state, global quests or content unlocks in **level\_progress** while storing personalized player progress information in **player\_progress**.

When the players connect the game would request **level\_progress&#x20;**&#x66;or all players and select the most advanced state. The game would also request **player\_progress**for all players and apply it to the corresponding player to unlock player specific aspects.

Such a hybrid achives the best results, if the corresponding game experiennce and game progression is optimized for AirConsole early in the game design and playtesting process, allowing you to create an experience your players want to come back to often and [increasing your income](https://developers2.airconsole.com/airconsole-hero-and-revenue).

### *Persisting data only for the controlling player*

If you are working with the controlling player (using <font color="#eb144c">`GetMasterControllerDeviceId()`</font>) to load the game state, then you need to consider the different cases arising from this Id changing as players join or leave the game.

:::BlockQuote
<font color="#eb144c">`GetMasterControllerDeviceId()`</font> can change during the runtime of the game due to connects / disconnects and you don't want to (re)load or store the data for the wrong player.
:::

**Examples**

- If a premium user connects to a game with non-premium users only, then the master device will change to the premium device.
- If the master device disconnects a different connected device will become master.
  Simply storing the persistent data to the now current master device would duplicate the state of the original players data to the new player while **not updating the progress for the original player**! The game needs to remember the UID of the device for which the game state has been requested and used.

### *Important considerations when not providing a UID to storePersistentData / requestPersistentData*

:::BlockQuote
From AirConsole API 1.9.0 and Unity Plugin 2.5.0 onwards, the possibility to call StorePersistentData / RequestPersistentData without providing a UID will be removed.
:::

If you are not providing a UID to StorePersistentData / RequestPersistentData, then the data will be stored for the device that calls the function.

This is a convenience functionality when using it the Persistent Storage API on your controller. You must not rely on this convencience when using it on the screen!

The screen does not have a persistent uid (incognito mode, security features, plugins). Due to this, the screen commonly is unable to restore persistent data reliably (browser and car in particular)!

## *Persisting data in web games*

The web examples assume that you have already [set up your game similar to the pong example](https://developers2.airconsole.com/pong-example) and have an object like <font color="#eb144c">`var airconsole = new AirConsole();`</font>.

### *Storing Persistent Data*

- Implement function \`handlePersistentDataStored\` that does something based on the UID, for example a visual confirmation.
- Register an event listener to <font color="#eb144c">`airconsole.onPersistentDataStored = handlePersistentDataStored;`
  </font>Alternatively you can also directly declare the function<font color="#eb144c">`airconsole.onPersistentDataStored = function(uid) { // do something based on the uid };`</font>
- After onReady was invoked, call

:::BlockQuote
&#x20;       string uid = airconsole.getUID(airconsole.getDeviceId());
&#x20;       if(uid) \{
&#x20;         airconsole.storePersistentData("dataKey", \{ keyA: "data a", keyB: "data b" }, uid);
&#x20;       } else \{
&#x20;         console.error("master controller has no UID yet, was onReady really invoked?");
&#x20;       }
:::

### *Retrieving Persistent Data*

- Implement function \`handlePersistentDataLoaded\` that processes the loaded persistent data.
- Register an event listener to <font color="#eb144c">`airconsole.onPersistentDataLoaded = handlePersistentDataLoaded;`
  </font>Alternatively you can also directly declare the function<font color="#eb144c">`airconsole.onPersistentDataLoaded = function(data) { // do something now with the data };`</font>
- After onReady was invoked, call something like

:::BlockQuote
&#x20;       var controllers = airconsole.getControllerDeviceIds();
&#x20;       var uids = controllers.map(it => airconsole.getUID(it)) || \[];
&#x20;       if(uids.length > 0) \{
&#x20;         airconsole.requestPersistentData(uids);
&#x20;       }
:::

## *Persisting data in Unity games*

Please see [ExampleBasicLogic.cs](https://github.com/AirConsole/airconsole-unity-plugin/blob/master/Assets/AirConsole/examples/basic/ExampleBasicLogic.cs) for an example implementation.

### *Storing Persistent Data*

- Implement an event handler \`HandlePersistentDataStored\` that processes the received persistent data.
- Register an event listener to <font color="#eb144c">`AirConsole.instance.onPersistentDataStored += HandlePersistentDataStored;`</font>
- After onReady was invoked, call

:::BlockQuote
&#x20;string uuid = AirConsole.instance.GetUID(AirConsole.instance.GetDeviceId());
&#x20;       if(!string.IsNullOrEmpty(uuid)) \{
&#x20;         JObject exampleData = new JObject();
&#x20;         exampleData.Add ("test", "data");
&#x20;         AirConsole.instance.StorePersistentData("example\_data", exampleData, uid);
&#x20;       } else \{
&#x20;         Debug.Error("MasterController has no UID yet, was onReady really invoked?");
&#x20;       }
:::

### *Retrieving Persistent Data*

- Implement an event handler \`HandlePersistentDataLoaded\` that processes the received persistent data.
- Register an event listener to <font color="#eb144c">`AirConsole.instance.onPersistentDataLoaded += HandlePersistentDataLoaded;`</font>
- After onReady was invoked, call something like

:::BlockQuote
&#x20;       List\<string> uids = new List\<string>(AirConsole.instance.GetControllerDeviceIds()
&#x20;         .Select(id => AirConsole.instance.GetUID(id))
&#x20;         .Where(id => !string.IsNullOrEmpty(id)));
&#x20;       if(uids.Count > 0) \{
&#x20;         AirConsole.instance.RequestPersistentData(uids);
&#x20;       }
:::

## *Persisting data does not work, I have the following problem:*

### *When reopening the game after storing the state, I can not get the stored state*

- The UIDs in the simulator are not stable. You will need to connect a real device to test this. See the guides [How to test your game](https://developers2.airconsole.com/testing-your-game) and [Learn how to connect Phones in Unity](https://developers2.airconsole.com/testing-with-your-phone) guides for more information.&#x20;
- You can not store or retrieve a persistent state before the event [onReady](https://airconsole.github.io/airconsole-api/) was invoked.&#x20;
  In Unity this would throw the [AirConsole.NotReadyException.](https://github.com/AirConsole/airconsole-unity-plugin/blob/b1c86de4fb1797f81d654051e6d61cc8c310b786/Assets/AirConsole/scripts/AirConsole.cs#L939)


[title] AirConsole Unity Plugin
[path] Games in Unity/

The Unity-Plugin is a C# wrapper for the AirConsole Javascript API. With the plugin, Unity serves you the needed screen.html file inside the Editor and creates a screen.html file after an WebGL build export.

:::BlockQuote
**Note:** The plugin comes with an embedded webserver / websocket-server for the communication between the AirConsole backend and the Unity-Editor. You don't need to install any other webserver or services. The plugin was tested with Unity 5.1 and above for the Windows and OSX-Editor.
:::

### *Known issues and solutions*

Before you start installing the plugin, be aware that because we support such a wide range of Unity versions, some Unity versions have some known issues when importing the plugin or when building for a specific platform. Below you will find the list of known issues and their fixes:

| Affected versions | Description                                                                                                                                                                                                                                                                                                                                                 | Solution                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Unity 2022+       | After trying to build for Android TV you may get this error:<br /><br /><font color="#eb144c">`mainTemplate.gradle is using the old aaptOptions noCompress property definition does not include types defined in unityStreamingAssets constant.`</font>                                                                                                     | Edit **Assets/Plugins/Android/mainTemplate.gradle** and **launcherTemplate.gradle** to add/replace the **aaptOptions** section to the following:<br />:::BlockQuote
aaptOptions \{&#xA;    noCompress = \['.ress', '.resource', '.obb'] + unityStreamingAssets.tokenize(', ')&#xA;    ignoreAssetsPattern = "!.svn:!.git:!.ds\_store:!\*.scc:.\*:!CVS:!thumbs.db:!picasa.ini:!\*\~"&#xA;}\*\*PACKAGING\_OPTIONS\*\*
:::<br /><br />Check [Unity support](https://developers2.airconsole.com/faq-help) for more details. |
| Unity 2021+       | After importing the AirConsole plugin into Unity you may get this error: <br /><br /><font color="#eb144c">`Multiple precompiled assemblies with the same name Newtonsoft.Json.dll included on the current platform. [...]`</font>                                                                                                                          | Delete the following plugin file (It is now included in Unity directly and is not needed in newer Unity versions): <font color="#eb144c">`Assets/AirConsole/plugins/Newtonsoft.Json.dll`</font>                                                                                                                                                                                                                                                                                                                         |
| Unity 2018.3      | After importing the AirConsole plugin into Unity you may get:<br /><font color="#eb144c">`The type or namespace name 'ILGenerator' could not be found (are you missing a using directive or an assembly reference?) and The type or namespace name 'DynamicMethod' could not be found (are you missing a using directive or an assembly reference?)`</font> | Set the **API Compatibility Level** to **.NET 4.x** in the Player Settings of your selected Platform.<br /><br />Check [AirConsole FAQ](https://developers2.airconsole.com/faq-help)  for more details.                                                                                                                                                                                                                                                                                                                 |

### *Download*

**Download from Github**

Download the newest AirConsole-Unity-Plugin on our [GitHub repository](http://github.com/AirConsole/airconsole-unity-plugin). You can find the newest ".unitypackage" file in the "Builds" folder.

**Download from Assetstore**

[Unity Store](https://assetstore.unity.com/packages/tools/game-toolkits/airconsole-42776)

### *Video Tutorial*

Learn the basics of how to make an AirConsole game in Unity in just 4 minutes.

::embed[]{url="https://www.youtube.com/embed/omKvqRZbfY8"}

### *Installation*

To import the plugin, click on <font color="#eb144c">`Assets > Import Package -> Custom Package`</font> and choose the downloaded <font color="#eb144c">`*.unitypackage`</font> file.

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/DLJWExYYGEHZ5ObQU96KQ_image.png" size="80" width="456" height="246" position="center" showCaption="false"}

Make sure that all assets are selected and click on "Import".

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/X8uCcFWbrtCI_jfMYyZ27_image.png" size="70" width="366" height="384" position="center" showCaption="false"}

**Instantiate AirConsole object**

To set up the communication inside a scene, you have to instantiate the AirConsole object. In the Hierarchy window, click on <font color="#eb144c">`Create > Create Other > AirConsole`</font>.

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/DoLR-NMNEqNINm-PjJUhD_image.png" size="60" width="350" height="270" position="center" showCaption="false"}

The AirConsole object offers you the following options:

| Option             | Description                                                                                                                                                                                                                                                                        |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Browser Start Mode | As soon as you press the “Play” button in Unity, your default browser will open the AirConsole simulator in the chosen mode.                                                                                                                                                       |
| Controller Html    | Link your controller.html file to this inspector variable, otherwise you can’t start the AirConsole simulator and you will get an error message. (Note: if your controller needs additional files like images or fonts, put them in the folder “Assets/WebGLTemplates/AirConsole”) |
| Auto Scale Canvas  | This option is enabled by default and scales your final webgl canvas automatically to the window size. The aspect ratio will be kept during the scaling.                                                                                                                           |

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/78kUQccxPrshav9aLnUWq_image.png" size="60" width="321" height="283" position="center" showCaption="false"}

:::BlockQuote
**Note:** the AirConsole object survives a scene change by default, so you don’t have to worry about the connections and device states when changing a level inside Unity).
:::

### *Quick example*

Here is a simple example where the controller sends "How are you?" to the screen and the screen replies with "Full of pixels!"

Include the AirConsole Javascript API in your controller.html file:

:::BlockQuote
\<script type="text/javascript" src="https\://www\.airconsole.com/api/airconsole-1.9.0.js">\</script>
:::

**/controller.html**

:::BlockQuote
var air\_console = new AirConsole();
air\_console.message(AirConsole.SCREEN, "How are you?");
:::

**Unity**

To be able to receive a message in Unity, create a new Unity MonoBehaviour class and include these namespaces:

:::BlockQuote
using NDream.AirConsole;
using Newtonsoft.Json.Linq;
:::

Add an event handler in the Start method of the MonoBehaviour class:

:::BlockQuote
void Start() \{
&#x20;   AirConsole.instance.onMessage += OnMessage;
}

void OnMessage(int from, JToken data) \{
&#x20;   AirConsole.instance.Message(from, "Full of pixels!");
}
:::

:::BlockQuote
**Note:** make sure to only send JSON supported data types between the controller.html file and Unity. The AirConsole-Unity-Plugin exchanges data via a modifed version of the popular JSON.NET library. The plugin delivers received data inside Unity as JToken objects. You can check the documentation of JSON.NET here: [http://www.newtonsoft.com/json/help/html/N\_Newtonsoft\_Json\_Linq.htm](http://www.newtonsoft.com/json/help/html/N_Newtonsoft_Json_Linq.htm)
:::

### *JToken parse example*

When you send an object like this one on the controller.html side:

:::BlockQuote
// Javascript
var message = \{
&#x20; 'action': 'move',
&#x20; 'info': \{ 'amount': 5, 'torque': 234.8 }
};

air\_console.message(AirConsole.SCREEN, message);
:::

You can parse the object in Unity like this:

:::BlockQuote
// C#
void OnMessage(int from, JToken data) \{
&#x20; string action = (string) data\["action"];
&#x20; int amount = (int) data\["info"]\["amount"];
&#x20; float torque = (float) data\["info"]\["torque"];
}
:::

:::BlockQuote
Note: when you receive JToken data in Unity, you have to cast the object to get the right data type.
:::

To send the same data from Unity you can use dynamic vars:

:::BlockQuote
// C#
var message = new \{
&#x20;   action = "move",
&#x20;   info = new \{ amount = 5, torque = 234.8f }
};

AirConsole.instance.Message(device\_id, message);
:::

### *Test your game*

To test your game, just hit the “Play” button inside the Unity-Editor. If you want to test the final WebGL-Build, choose the right AirConsole Template in the PlayerSettings first (For Unity versions 2020 and up choose the **AirConsole-2020** template, for previous Unity versions choose the **AirConsole** template):

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/0A4IF7aN04TvX6GBCPwQm_image.png" size="80" width="400" height="379" position="center" showCaption="false"}

Now you can export your Unity project in the Build-Settings (choose “Build” instead of the “Build and run” button). As soon as your build is exported, the button “Open Exported Port” on the AirConsole object in your scene will appear. By pressing this button, the AirConsole simulator will start with your final build.

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/rbRSVhPH6n-smRLYYvL54_image.png" size="50" width="250" height="178" position="center" showCaption="false"}

:::BlockQuote
**Note:** the "Open Exported Port" button is only available as long as you don’t change the last target location of your exported build.
:::

:::BlockQuote
**Note:** AirConsole uses the Unity WebGL Custom Progress Bar by Alexander Ocias for WebGL builds: https\://ocias.com/
:::

:::BlockQuote
**IMPORTANT:** before you can build the WebGL port, you have to start the game at least once in the Editor to generate all important metafiles for the final build.
:::

### *Examples*



- AirConsole/Examples/Basic/basic.scene:
  In this example all possible API functions are listed & used.

- AirConsole/Examples/Pong/pong.scene:
  This example contains a simple pong game with a specific controller.

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/uOouAd15U-Yv5oP61kD32_image.png" size="60" width="895" height="557" position="center" showCaption="false"}

### *Settings*

You can open the settings window by clicking on the "Settings" button on the inspector of the AirConsole object or by navigating over the menu bar <font color="#eb144c">`AirConsole > Settings`</font>.

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/qXF2nNM3Genls_VRzoHra_image.png" size="60" width="417" height="147" position="center" showCaption="false"}

In the "Settings" window, the ports for the internal web socket and web server can be changed, should the default ports already be occupied.

:::BlockQuote
**Note:** These ports are only needed for the AirConsole simulator inside the Unity Editor. The final WebGL port will not use any of these ports.
:::

You can stop and restart the internal web server in case the AirConsole simulator isn't working properly.

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/rEp6aJltjg85SAZsIlk6E_image.png" size="60" width="336" height="278" position="center" showCaption="false"}

### *AirConsole, Unity and AndroidTV*

AirConsole also provides an AndroidTV app, so that user can play your games also on a smart-tv. Please read the ["AirConsole Unity on AndroidTv"](https://developers2.airconsole.com/unity-game-on-android-tv) documentation to adapt your game for AndroidTV.

[title] Creating and publishing your game
[path] AirConsole Games Features/


[title] Archive
[path] /


[title] Pong Example
[path] Example games in web / unity/

In this example you will learn how to use the AirConsole API to communicate between a screen and controllers in a simple Pong-game.

You can play the game here: [https://www.airconsole.com/#https://game.airconsole.com/com.airconsole.testpong.cdn.airconsole.com/2024-02-14-14-52-40/](https://www.airconsole.com/#https://game.airconsole.com/com.airconsole.testpong.cdn.airconsole.com/2024-02-14-14-52-40/) to get a better idea about what we are gonna talk about :)

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/UQ_-qjNU_Gc1nxoiayV_V_image.png)

You can check the game code here: [View code.](https://github.com/AirConsole/games-pong)

To download the game, use the following link: [Download pong.zip](https://github.com/AirConsole/games-pong/archive/master.zip)

### *Basic message exchange between a screen and controllers:*

AirConsole and its API makes it very easy for game developers to build games, which can be controlled by a smartphone - instead of e.g. a keyboard or a mouse.

The whole communication can be pretty much summed up with two methods of the API:<font color="#eb144c">`onMessage()`</font>and<font color="#eb144c">`message()`</font>.

All devices (screen and controllers) share the same API - thus there are (almost) no controller or screen specific methods for the communication.

In the following we will explain you in detail how players can control their paddles in a simple, but fun game like Pong.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/WtnChVPLUjodP-lYvjRsK_image.png)

### */screen.html*

Start a local webserver of your choice and create a <font color="#eb144c">`/screen.html`</font> in the root directory.

:::BlockQuote
You can check the code of the whole <font color="#eb144c">`screen.html`</font> in [this repo](https://github.com/AirConsole/games-pong/blob/master/screen.html)
:::

Include the AirConsole API file:

|   |     \<script type="text/javascript" src="https\://www\.airconsole.com/api/airconsole-1.7.0.js">\</script> |
| - | --------------------------------------------------------------------------------------------------------- |

Our game will run on a canvas element, have a score and a "wait message". As soon as the document is ready, we call the<font color="#eb144c">`init()`</font>function.

```html
<body onload="init()">
    <div class="game">
      <canvas id="canvas"></canvas>
      <div id="score">0:0</div>
      <div id="wait"></div>
    </div>
  </body>
```

The<font color="#eb144c">`init()`</font>function sets up our canvas, creates player objects and a ball object and most importantly calls the<font color="#eb144c">`setupConsole()`</font>function.

In the<font color="#eb144c">`setupConsole()`</font>we construct the<font color="#eb144c">`AirConsole`</font>object and assign it to the<font color="#eb144c">`airconsole`</font>variable.

```javascript
//* Sets up the communication to game pads.* /
function setupConsole() {
  airconsole = new AirConsole();
  airconsole.onConnect = function (device_id) {
    checkTwoPlayers();
  };
  airconsole.onDisconnect = function (device_id) {
    var player = airconsole.convertDeviceIdToPlayerNumber(device_id);
    if (player != undefined) {
      // Player that was in game left the game.
      // Setting active players to length 0.
      airconsole.setActivePlayers(0);
    }
    checkTwoPlayers();
  };
  airconsole.onMessage = function (device_id, data) {
    var player = airconsole.convertDeviceIdToPlayerNumber(device_id);
    if (player != undefined && data.move !== undefined) {
      paddles[player].move.y = data.move;
    }
  };
}
```

After that, we overwrite the<font color="#eb144c">`onMessage()`</font>,<font color="#eb144c">`onConnect()`</font>and the<font color="#eb144c">`onDisconnect()`</font>methods, so we can use them for our needs of the game.

<font color="#eb144c">`onMessage(device_id, data)`</font>

The<font color="#eb144c">`onMessage(device_id, data)`</font>method listens for all incoming messages. In the screen-context it means that it receives all data which the controllers send. In our pong-game, whenever a player presses the up or down button on its device, the screen receives a messages and that is handled in the<font color="#eb144c">`onMessage(device_id, data)`</font>method. The<font color="#eb144c">`device_id`</font>param is the id of the device which sent the message (E.g.*2*). The<font color="#eb144c">`data`</font>param is an object and holds custom data, like<font color="#eb144c">`{ move: 50 }`</font>.

<font color="#eb144c">`onConnect(device_id)`</font>

The<font color="#eb144c">`onConnect()`</font>method gets called when a device connects.

<font color="#eb144c">`onDisconnect(device_id)`</font>

Whenever a device disconnects<font color="#eb144c">`onDisconnect()`</font>gets called.

<font color="#eb144c">`setActivePlayers(max_players)`</font>

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/uVfEFdzRQHMa-SSDTP8dT_image.png)

The<font color="#eb144c">`setActivePlayers(max_players)`</font>was introduced to avoid confusion with device ids. You can use it as a helper method which tells which devices should get a player number. It basically maps your controller devices to a zero-based player number.

**Set active players**

When a player connects and we have at least two controllers connected, we call<font color="#eb144c">`setActivePlayers(2)`</font>method in the<font color="#eb144c">`checkTwoPlayers()`</font>function to set the first two devices to active players and start the game.

```javascript
// Only update if the game didn't have active players.
if (active_players.length == 0) {
  if (connected_controllers.length >= 2) {
    // Enough controller devices connected to start the game.
    // Setting the first 2 controllers to active players.
    airconsole.setActivePlayers(2);
    resetBall(50, 0);
    score = [0, 0];
    score_el.innerHTML = score.join(":");
    document.getElementById("wait").innerHTML = "";
  } else if (connected_controllers.length == 1) {
    document.getElementById("wait").innerHTML = "Need 1 more player!";
    resetBall(0, 0);
  } else if (connected_controllers.length == 0) {
    document.getElementById("wait").innerHTML = "Need 2 more players!";
    resetBall(0, 0);
  }
}
```

Otherwise we just display that more players are needed to play the game.

**Set no active players**

When an active player disconnects and there are not enough active players left we want again to display "*It's a 2 player game!*" on the controller and on the screen "*Need X more player!*". When the leaving player was active in the game, we set all active players to 0, before calling again the<font color="#eb144c">`checkTwoPlayers()`</font>function.

<font color="#eb144c">`message(device_id, data)`</font>

The message method sends a message to another device. In our pong game, whenever a player wins, we send a message to this device and let the device vibrate (if supported).

```javascript
/**
 * Sends a message to the device that it should vibrate;
 */
function vibrate(player) {
  airconsole.message(
    airconsole.convertPlayerNumberToDeviceId(player),
    { vibrate: 1000 })
}
```

How that works is explained below in the controller.html.

Thats basically all AirConsole relevant code of the<font color="#eb144c">`screen.html`</font>. The rest of the code is only the game itself, which we won't cover here.

### */controller.html*

Whenever someone requests an AirConsole game on a mobile device, the <font color="#eb144c">`/controller.html`</font> will be delivered.

Now create a<font color="#eb144c">`/controller.html`</font>and include the same AirConsole API file as we did in the<font color="#eb144c">`/screen.html`</font>:

```html
<script type="text/javascript" src="https://www.airconsole.com/api/airconsole-1.7.0.js"></script>
```

As in the<font color="#eb144c">`/screen.html`</font>, we also create an<font color="#eb144c">`AirConsole()`</font>object and initialize it with the options<font color="#eb144c">`{"orientation": "portrait"}`</font>.

```javascript
/**
 * Sets up the communication to game pads.
 */
function setupConsole() {
  airconsole = new AirConsole();

  airconsole.onConnect = function (device_id) {
    checkTwoPlayers();
  };

  airconsole.onDisconnect = function (device_id) {
    var player = airconsole.convertDeviceIdToPlayerNumber(device_id);
    if (player != undefined) {
      // Player that was in game left the game.
      // Setting active players to length 0.
      airconsole.setActivePlayers(0);
    }
    checkTwoPlayers();
  };

  airconsole.onMessage = function (device_id, data) {
    var player = airconsole.convertDeviceIdToPlayerNumber(device_id);
    if (player != undefined && data.move !== undefined) {
      paddles[player].move.y = data.move;
    }
  };
}
```

We again overwrite the<font color="#eb144c">`onMessage()`</font>method and also make use of the<font color="#eb144c">`onActivePlayersChange()`</font>method.

<font color="#eb144c">`onMessage(device_id, data)`</font>

This time we listen for all messages, which were sent from the screen. As already mentioned, whenever a player wins, the device of the player should vibrate. The<font color="#eb144c">`onMessage(device_id, data)`</font>method receives now a message from the screen (AirConsole.SCREEN) and with the data<font color="#eb144c">`{ vibrate: 1000 }`</font>.

<font color="#eb144c">`onActivePlayersChange(player_number)`</font>

Whenever the screen calls<font color="#eb144c">`setActivePlayers()`</font>, the<font color="#eb144c">`onActivePlayersChange(player_number)`</font>method will be called on the controllers, which are set to active. Use this method to tell your controller it is an active device in your game. In our Pong game we use the passed<font color="#eb144c">`player_number`</font>to tell if the player is the left or the right paddle. The<font color="#eb144c">`player_number`</font>starts at 0, so that the first player will receive *0&#x20;*&#x61;nd the second *1&#x20;*&#x61;s the<font color="#eb144c">`player_number`</font>param.

**Sending a move event from controller to the screen**

To tell the screen that a player wants to move his paddle up or down we use the<font color="#eb144c">`airconsole.message`</font>method.

```javascript
/**
 * Tells the screen to move the paddle of this player.
 * @param amount
 */
function move(amount) {
  airconsole.message(AirConsole.SCREEN, {move: amount})
}

```

On an<font color="#eb144c">`ontouchstart`</font>event the move function will be called, which sends a message to the screen. On a<font color="#eb144c">`ontouchend`</font>we again call the move function with a value of 0 and again send a message to the screen.

```html
<div class=button ontouchstart="move(-50)" ontouchend="move(0)" onmousedown="move(-50)" onmouseup="move(0)">
  <div class="button_label">UP</div>
</div>
<div style="height: 8%"></div>
<div class=button ontouchstart="move(50)" ontouchend="move(0)" onmousedown="move(50)" onmouseup="move(0)">
  <div class="button_label">DOWN</div>
</div>
```

That's it! That's all you need to let your screen and controllers communicate!

Learn more about [how to test your game](https://developers2.airconsole.com/testing-your-game) locally or on your webhost.

[title] Handling players connecting
[path] AirConsole Games Features/Connect to your game/

:::BlockQuote
Prerequisites: API 1.9.0 or Unity Plugin 2.5.0 or higher
:::

## *What is Player Silencing?*

Player Silencing allows you to control what happens when a new player joins the game while the game is in an active game round without additional game code.

It ensures that newly joined players are not known to the game until the game round is finished.

:::BlockQuote
Player Silencing is by default not enabled! If you update your game to use the new API it will not impact your game until you decide to enable it.
:::

### *Comparison*

**With player silencing disabled:&#x20;**&#x4E;ew players will be added to your game as soon as they connect to the game, invoking onConnect and requiring your game to manually handle and ignore messages as well as visualizing the correct state on the game controller.

**With player silencing enabled:&#x20;**&#x4E;ew players will be informed that they need to wait for the game round to finish on their controller.
Your screen and controller will only invoke onConnect after the game round is no longer active, which is controlled by your screen calling setActivePlayers(0).

## *How does Player Silencing work?*

Player Silencing works by keeping track of new devices connecting while the game is in an active game round.

The onConnect event of such devices will be delayed until the game round is no longer active.

Once the active game round is no longer active, all waiting onConnect events will be executed, **unless&#x20;**&#x74;he device has disconnected already.

## *Who is Player Silencing for?*

Player Silencing allows you to be sure that after a game round is started, newly joining players are requested to wait until the game round is finished. This does not require additional game code to prevent an impact to the game experience or the game rounds outcome.

## *When not to use Player Silencing?*

If your game is designed to allow players to join at any time, then player silencing is not for you.

## *How to guides*

### *How to: **Enable Player Silencing***

To enable and use player silencing, all you need to do is to configure AirConsole to use it on the screen.

- **Web Setup:** Include <font color="#eb144c">`silence_inactive_players: true`</font> when calling the constructor:

:::BlockQuote
var airconsole = new AirConsole(\{ silence\_inactive\_players: true });
:::

- **Unity Setup:&#x20;**&#x45;nable the checkbox<font color="#eb144c">`Silence Players`</font>on the AirConsole component in the scene.

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/L3gEEzW6h4GKdEukxKEmf-m8sn7HURyoTysiDTrnB7p-20250303-115802.jpg" size="60" width="443" height="364" position="center" showCaption="false"}

### *How to: **Use Player Silencing***

After enabling playing silence, you can control when new players can join using <font color="#eb144c"></font>.

Once player silencing is enabled, during any phase of your game where you <font color="#eb144c"></font> to anything but 0, new joining players will be silenced.
After <font color="#eb144c"></font> is called, the game will be in a waiting state and new players will be added to the game.
Players that are already waiting to join will now be added and onConnect is called for them.

## *The example pong game explained*

We will use the [Pong Example Game](https://github.com/AirConsole/games-pong), to explain player silencing and how it applies :

1. We initialize AirConsole including the<font color="#eb144c">`silence_inactive_players: true`</font>flag when creating the AirConsole object, signaling that new players to automatically wait during active gameplay.
2. The game is initially in the LOBBY state, waiting for players.
3. Once 2 devices are connected and the master device starts the game, the game switches to the GAME state.
   - Entering the GAME state executes <font color="#eb144c">`setActivePlayers(2)`</font>, starting the active Pong gameplay.
   - If any additional device connects while in the GAME state, the new device will be silenced and the controller will inform the user with the following screen.
4. After the game ends, the screen enters the LOBBY state, which calls <font color="#eb144c">`setActivePlayers(0)`</font> to mark no controller as active player.
   - After setActivePlayers(0) all players that joined while the GAME was ongoing will now join the game, showing the currently active controller

We provide a full example implementation as a 2 player Pong game with dedicated game states.

### *Game States*

- **LOBBY**: New players automatically join the pre-game waiting state until the main controller decides its time to start.
  In this state, setActivePlayers(0) was called.
- **GAME:&#x20;**&#x49;n this state, new players need to wait to join (they are **silenced**).
  In this state, setActivePlayers(2) was called.

You can find this example Pong Game at [https://github.com/AirConsole/games-pong](https://github.com/AirConsole/games-pong)

[title] AirConsole JS API
[path] Reference/


[title] Explanation
[path] /


[title] Best Practices for Web Games on AirConsole
[path] /

This guide provides developers with essential **guidelines and standards** for building performant and reliable WebGL games for AirConsole. The document applies to **desktop, mobile, and automotive** devices. Games are expected to **scale automatically** across platforms — there are no separate versions.

***

## 🎯 Goals

- Ensure compliance with [AirConsole Checklist](docId\:IEdkpmfJl8JQS3UOIFhmg).
- Provide **performance and resource usage best practices**.
- Deliver games that are **robust, safe, and enjoyable** everywhere.

***

## ✅ Technical Requirements

### General Requirements

- **Compatibility:** Game must run correctly on **Chrome 61+** (baseline automotive embedded Chromium).
- **Loading Performance:** Main menu loads in ≤30s under 20mbit up/down with 100ms latency.
- **Frame Rate:** Game must achieve **≥30 FPS** consistently across all supported platforms.
- **Error Handling:** No uncaught exceptions in console during normal play.

:::hint{type="success"}
Always validate performance under throttled conditions, not just on your development machine.
:::

***

## 🚀 General Performance Best Practices

### Rendering — Lights, Shaders & Materials

Automotive graphics chip are heavily pixel throughput limited (up to 20x lower than current generation mobiles)&#x20;

- **Prefer baked lighting** for static geometry; reserve **1–2 realtime dynamic lights** for hero objects only. Use **lightmaps** (directional if supported) or **spherical harmonics light probes** for dynamic objects.
- **Shadow strategy:** If shadows are required, use **single sun/dir light shadows** with **resolution ≤1024** and **cascade count ≤2**; disable soft shadows on low-end. Avoid per-pixel point/spot shadows.
- **Shader precision:** Default to `mediump` in fragment shaders; use `highp` **only** where artifacts are visible (normals/IBL). In vertex shaders, `highp` is generally fine.
- **Branching & loops:** Avoid dynamic branching and unbounded loops in fragment shaders; prefer **static&#x20;**`#define`**-based variants** and precomputed LUTs (BRDF, fog, tone map curves).
- **Material variants:** Limit material permutations; prefer a **single uber-shader** with compile-time feature flags, or a **small set** of well-curated variants.
- **Specular/IBL:** Use **image-based lighting** with prefiltered environment maps (mip chain as roughness levels). Keep environment cubemap at **≤256 px face** (≤128 on low-end / automotive).
- **Normals & tangents:** Use **octahedral/BC5**-style normal encodings when possible; otherwise compress normals to two channels. Recompute tangents offline.
- **PBR pragmatism:** Avoid it unless it provides significant value. If used, use **metalness/roughness** workflow; clamp roughness to **\[0.04, 0.96]** to avoid fireflies.
- **Tone mapping & gamma:** Avoid HDR and tonemapping unless it provides significant value. If used work in **linear space** with sRGB textures; apply **ACES (approx)** or filmic tone mapping **once** at the end of the pipeline.
- **Transparency:** Sort back-to-front; minimize overdraw. Prefer **alpha test / dithered cutout** over expensive blended layers when possible.
- **Post-processing budget:** Keep to **≤1–2 passes**. Prefer **FXAA** over MSAA;&#x20;
  - Ensure mobile optimised shaders are used when running on cars
- **Instancing & batching:** Batch small meshes and **GPU-instance** repeated props to keep draw calls **\< 200**.
- **Overdraw control:** Keep fill rate low; avoid full-screen particles and parallax layers. Use **depth pre-pass only if profiling proves a net win**.
- **Texture sampling:** Prefer **trilinear with mipmaps**; add **anisotropy 2–4x** for ground textures. Avoid manual LOD biasing unless necessary.
- **Texture lookups:** For wide support try to have \<= 4 texture l
- **Skinned meshes:** Limit bones per draw (≤50); update skinning on GPU where available; pool and reuse skeletons.
- **GL Extensions**: The available GL extensions vary widely between platforms, in particular in cars -> Ensure you only use features that are provded by the GL extensions
  - If that is technically not feasible -> Ensure that you support WebGL1

:::hint{type="success"}
Baked/global lighting + minimal dynamic lights yields the biggest visual/performance win for WebGL across all devices.
:::

### Assets — Formats, Sizes & Streaming

- **Texture formats:** Ship GPU-compressed textures: **ASTC** (best), **ETC2** (fallback AndroidTV) and **DXT** for desktop.
  - Where available use&#x20;**&#x20;Basis** or **KTX2** which are optimal for automotive and desktop.
- **Texture sizes:** Target **max 2048²** (automotive) / 4096² (desktop high). Generate full mip chains; avoid NPOT when possible to keep mips efficient.
- **Atlasing:** Use **sprite/texture atlases** to reduce binds; group by sampler/state.
- **Streaming:** Stream large assets (music, video) and **lazy-load** level content; gate heavy loads behind user interactions.
- **Audio:** Stream BGM; keep SFX short and pooled. Avoid decoding large audio during gameplay.

### Memory Management

- Cap texture memory per platform (\~256MB for low-end & automotive, higher for desktop).
- Implement **lazy asset loading**: load resources only when needed.
- Use **asset eviction strategies**: unload unused textures/models/audio.
- Monitor garbage collection behavior; avoid excessive object churn in critical loops.

:::hint{type="warning"}
Run memory profiling sessions at least once per major milestone — many issues only appear under long play sessions.
:::

### Particles & VFX

- Keep particle systems **GPU-cheap**: billboard quads, batched draws, avoid complex fragment work.
- Prefer **CPU-updated, GPU-drawn** pools
- Avoid allocating particle and VFX systems during gameplay
- Cull off-screen particle and VFX systems early.
- For smoke/fire, use [flipbook textures / texture sheets](https://vfxdoc.readthedocs.io/en/latest/textures/flipbooks/) at half/quarter-res
- Limit additive blending areas using tight sprite meshes where possible

### Input & Responsiveness

- Ensure **input latency \<100ms** under normal conditions.
- Support **AirConsole's input integration** only.
- Design for **scalable UI layouts** — support resizing and different aspect ratios.
- **If your game's input is time sensitive:&#x20;**&#x55;se lag compensation approaches and timestamps for controller -> screen messages for the best game experience.

### Audio

- Use **compressed audio formats** (AAC low, OGG) for background music.
- Keep audio memory low by streaming long tracks.
- Ensure to implement onPause/onShowAd -> onResume/onAdComplete to pause and resume audio.

### Power & Battery

- Minimize unnecessary background work.
- Pause heavy animations or physics simulations when not visible.
- Use **frame skipping** or **dynamic resolution scaling** if hardware struggles.

***

## 🚗 Automotive-Specific Considerations

While games must scale automatically across all platforms, **automotive environments** require additional care:

- Avoid distracting animations or high-contrast flashing elements.
- Handle **OS memory pressure** → free unused textures/models.
- Implement **crash recovery**: auto-return to safe menu state.
- Test readability under glare and low-light conditions.

***

## 🧪 Testing & Certification

### Mandatory Test Scenarios

- Chrome DevTools Network Throttling (20mbit/100ms latency).
- FPS measurement with Chrome Performance tab.
- Memory profiling vs platform budgets.
- Long play session stability (≥1 hour).
- AirConsole simulator and real-device tests (incl. automotive hardware where possible).

### Recommended Tooling

- Chrome DevTools (Performance, Memory, Network tabs).
- WebGL Inspector / Spector.js for GPU analysis.
- For automotive, test using the [Xiaomi Pad 6 WLAN](https://www.gsmarena.com/xiaomi_pad_6-12237.php)
  - **For web:** To test your game in a similar performance setup as many cars, use Chrome to load [https://airconsole.com](https://airconsole.com) as Desktop Site and connect the Chrome Dev tools with video stream active. This gives a somewhat similar experience and performance profile.
  - **Android native games:** Work like normal but ensure to not add your game to Xiaomi's Game Space! This would enable optimisations that impact performance and behaviour considerably.

***

## ⚖️ Do’s and Don’ts

| Do ✅                                                   | Don’t ❌                                   |
| ------------------------------------------------------ | ----------------------------------------- |
| Use compressed textures and atlases.                   | Ship raw PNGs or too many small textures. |
| Profile with Chrome DevTools and Spector.js.           | Assume desktop performance = automotive.  |
| Implement adaptive quality (LOD, texture scaling).     | Hardcode high-quality settings.           |
| Test input latency on real devices (incl. automotive). | Only test on desktop Chrome.              |
| Stream large assets (audio/video).                     | Load everything eagerly at startup.       |

***

## 📌 Summary

- Meet **technical requirements** across all platforms.
- Follow **general performance best practices**: rendering, networking, memory, input, audio, and power.
- Apply **automotive-specific safety considerations** when needed.
- Use robust **testing workflows and profiling tools** to ensure consistent quality.

By following this guide, developers can deliver games that are performant, efficient, and enjoyable across all environments on AirConsole.

[title] Example games in web / unity
[path] /


[title] FAQ / Help
[path] Reference/

## General

:::ExpandableHeading
### Where can I publish a game in the gamestore?

Visit [https://www.airconsole.com/developers](https://www.airconsole.com/developers) or visit the "[How to publish your game](https://developers2.airconsole.com/publishing-your-gamehttps://developers2.airconsole.com/publishing-your-game)" guide.
:::

:::ExpandableHeading
### Where can I get information about the monetization?

You can find information about the monetization in our "[AirConsole Hero & Revenue](https://developers2.airconsole.com/airconsole-hero-and-revenue)" guide.
:::

:::ExpandableHeading
### Which games can be built for AirConsole?

If your game runs in a browser then it also works with AirConsole. In fact you won't write a lot of code to adapt your game for AirConsole. However, there are of course limitations to consider like for example the latency or the amount of buttons your controller should have. Check our "[Best Practices for Web Games on AirConsole](docId\:Due8W48UINjBRxskf8iNf)" to see what other developers have built for AirConsole.
:::

:::ExpandableHeading
### Who is behind AirConsole?

Checkout these handsome people at [N-Dream AG](https://corp.airconsole.com/).
:::

## Technical

:::ExpandableHeading
### How can I test my game?

Please check out the "[Testing your game](https://developers2.airconsole.com/testing-your-game)" guide.
:::

:::ExpandableHeading
### Can I directly link to my game?

Yes, by either using https\://www\.airconsole.com/#http\://your-game-url/ or when your game is published in the game store by https\://www\.airconsole.com/#!play=GAME\_ID.
:::

:::ExpandableHeading
### Is there an easy way to simulate high latency?

Yes, if you load `www.airconsole.com/?server=[SERVER_ZONE]`or `www.airconsole.com/simulator/?server=[SERVER_ZONE]`all your communication will go through a datacenter in the specified server zone. WebRTC will also be disabled. To simulate high latency, choose a server zone that is far away. Currently available server zones are:`europe-west1-b`,`us-central1-a`,`asia-east1-a`.

To show the latency of your controllers, you can use the [Latency Debugger](https://developers.airconsole.com/#!/help#technical-latency:~\:text=can%20use%20the-,Latency%20Debugger,-.).
:::

:::ExpandableHeading
### What is the send-message limit?

Between two devices, there is a limit of 25 messages per second (e.g. from one controller to the screen).

Broadcasts and Custom device state changes (combined) are limitted to 25 events per second per device.

However, all messages will be queued, so that no message will be lost. We've created a [Rate Limiter](https://github.com/AirConsole/airconsole-controls/tree/master/rate-limiter) library to help you to rate-limit messages intelligently.
:::

:::ExpandableHeading
### I can't load my game in the simulator or on a test phone!

Make sure your`screen.html`and`controller.html`are accessible from all devices.

Let's assume you are running your local web server on http\://192.168.0.2:8080/
Try to access http\://192.168.0.2:8080/screen.html and http\://192.168.0.2:8080/controller.html using a normal web browser on the device that doesn't work. It should display the screen/controller html.
If you can't access http\://192.168.0.2:8080/controller.html from your phone, but from your computer, make sure you are in the same network and that your router does not have "Client Isolation" activated.
If you are testing with real smartphones, make sure you do not use http\://localhost/ or http\://127.0.0.1/, because localhost is not your computer on the phone but the phone itself!

If you are still unable to connect your phone, you can use the "ngrok" tool to forward your local url. See our [guide](https://developers2.airconsole.com/testing-with-your-phone) for how to do this in the Unity Engine.
:::

:::ExpandableHeading
### Is WebRTC supported?

Yes, we support WebRTC but only as much as WebRTC [is currently supported](http://iswebrtcreadyyet.com/) by browsers. WebRTC decreases the latency dramatically. Because not everyone supports it yet, always assume that your game also works on a higher latency. When WebRTC is supported you will see a message`WebRTC active for device_id {id}`in the browsers console.
:::

:::ExpandableHeading
### Can I self-host AirConsole?

No that is not possible. But you can directly link to your game like this: http\://www\.airconsole.com/#*http\://your-game-url/.*
:::

## Monetization, Ads & AirConsole Hero

:::ExpandableHeading
### How are games on AirConsole monetized?

Users can purchase our premium subscription, [AirConsole Hero](https://www.airconsole.com/hero). Please see the [monetization guide](https://developers2.airconsole.com/airconsole-hero-and-revenue) for details.
:::

:::ExpandableHeading
### Can I create special content for Hero users?

Yes, can lock some content in your game (like characters, or certain levels or game modes) so that they’re only available if at least one Hero user is playing.

Feel free to contact us if you need ideas on how to incorporate Hero content into your game. See our [guide on Premium content](https://developers2.airconsole.com/hero-exclusive-content) for more information.
:::

::::ExpandableHeading
### How do I show ads?When should I show ads?

By calling the`showAd`method in the AirConsole API.

:::BlockQuote
Make sure you mute the sound and execution of the game if`onAdShow`is called.
You can unmute sounds and game execution as soon as`onAdComplete`is called.
:::
::::

:::ExpandableHeading
### When should I show ads?

It is up to you to find the best moment in your game to show an ad. We recommend calling the function at a point where there is a sort of ‘pause’ in your game, but before it’s actually over. Because we will limit the frequency of when an Ad is actually shown on our side to every 5 minutes, you should try to display ads as often as possible.
:::

:::ExpandableHeading
### Do I get ad revenue from my game on AirConsole?

There is no ad revenue on AirConsole. Any "ad breaks" that are shown are just advertising our own AirConsole Hero subscription, or showing other AirConsole features or content. The platform does not get any income through third party advertisements, and neither do developers.
:::

:::ExpandableHeading
### How do I get paid?

We pay developers via Paypal or Bank Wires. You can request a payout on your Revenues page, which you can access via the [Developers Console](https://www.airconsole.com/developers).
:::

:::ExpandableHeading
### Can I decide which ad or kind of ad will be shown?

No. All ads that are shown on the platform are our own AirConsole Hero advertisements. The exact content and order of advertisements that players see is carefully balanced on AirConsole's side.
:::

:::ExpandableHeading
### Why don't you show third party advertisements?

We did at some point and our data clearly showed that our users were happier and more likely to get an AirConsole Hero subscription if no external ads were shown. The ad revenue gained by impressions was not worth it for us in comparison.
:::

::::ExpandableHeading
### Can I launch a free/ad-free/unmonetized game on AirConsole?

Normally, no. Since we have server costs from all the games that are live on the platform, we don't allow anyone to launch completely unmonetized games. For special solutions and exceptions to this (are you making a game for a good cause, for example?) get in touch [via email](https://publishers@n-dream.com).

## Unity

:::ExpandableHeading
### Where do I add images to my controller?

Any files your controller should have access to, such as images, JavaScript files, style sheets etc., should be placed either in`Assets/WebGLTemplates/AirConsole-2020`for Unity versions 2020 and up or in`Assets/WebGLTemplates/AirConsole`for Unity versions 2019 and down.
The controller that is linked in your AirConsole Game Object will be automatically copied to that folder when you start your game, and paths will be relative to it.
:::

:::ExpandableHeading
### Why do I get compilation errors after adding JavaScript files?

If your JS scripts throw compilation errors, it's because Unity is trying to compile them as UnityScript files. Make sure you place all JavaScript files in`Assets/WebGLTemplates/AirConsole`. If the compiler errors show up despite proper file location, try restarting the Unity Editor.
:::

:::ExpandableHeading
### Why do I get "Multiple precompiled assemblies with the same name Newtonsoft.Json.dll included on the current platform. \[...]" after importing the AirConsole plugin in Unity 2021+

Delete the following plugin file (It is now included in Unity directly and is not needed anymore in newer Unity versions):`Assets/AirConsole/plugins/Newtonsoft.Json.dll`
:::

:::ExpandableHeading
### Why do I get "mainTemplate.gradle is using the old aaptOptions noCompress property definition does not include types defined in unityStreamingAssets constant." when building for Android TV on Unity 2022+?

Unity 2022 made some updates to their gradle launcher which doesn't support and old option syntax anymore. The solution is to edit **Assets/Plugins/Android/mainTemplate.gradle&#x20;**&#x61;nd **launcherTemplate.gradle&#x20;**&#x74;o add/replace the **aaptOptions&#x20;**"noCompress" line with`noCompress = ['.ress', '.resource', '.obb'] + unityStreamingAssets.tokenize(', ')`

Check [Unity Support](https://support.unity.com/hc/en-us/articles/4408584577044-Why-do-I-get-errors-when-using-a-Gradle-file-with-an-old-aaptOptions-noCompress-property) for more details.
:::
::::

## Help and contact

Chat with the AirConsole team and other fellow developers on our [Discord](https://discord.com/invite/5BzND3v) channel.

If you have any ideas for how to make AirConsole better and more attractive for game developers, let us know your thoughts!
Fill out our [Developer Feature Request Form here](https://goo.gl/forms/0ezPM42bL082dBH93).

[title] Using the microphone to create unique games
[path] AirConsole Games Features/Improving your game engage/

This guide is for game developers using `airconsole-1.11.0.js` inside their game.

Use `airconsole.getUserMedia()` when a controller needs microphone access, for example for voice volume, singing, blowing, rhythm, or other audio-reactive gameplay.

## Quick Start

Add a button to your controller HTML:

```html
<button id="request-microphone" type="button">Enable microphone</button>
<p id="microphone-status"></p>
```

Then wait until AirConsole is ready and request microphone access from that button. In most games, requesting from an intentional player action is clearer than asking immediately on page load and various browsers require user interaction for permission requests to work:

```javascript
var micStream = null;

function setMicrophoneStatus(message) {
  document.getElementById("microphone-status").textContent = message;
}

airconsole.onReady = function() {
  document.getElementById("request-microphone").onclick = function() {
    airconsole.getUserMedia({ audio: true })
      .then(function(stream) {
        micStream = stream;
        setMicrophoneStatus("Microphone enabled");
        // Use the stream, for example with the Web Audio API.
      })
      .catch(function(error) {
        // Show a useful message or keep the game playable without microphone input.
        setMicrophoneStatus("Microphone is not available");
        console.error("Microphone access failed:", error);
      });
  };
};
```

Stop the microphone when you no longer need it:

```javascript
if (micStream) {
  micStream.getTracks().forEach(function(track) {
    track.stop();
  });
  micStream = null;
}
```

Stopping tracks matters. It releases the microphone for the browser or app and lets privacy indicators turn off when nothing else is using the microphone.

## Current limits

As of 2026-05-19, the AirConsole API 1.11.0 supports getUserMedia only on controllers and it only supports the audio constraint, not video

```javascript
// Rejects in AirConsole 1.11.0.
airconsole.getUserMedia({ video: true });

// Rejects in AirConsole 1.11.0 on the screen.
airconsole.getUserMedia({ audio: true });
```

## What You Can Request

In AirConsole 1.11.0, you can call

```javascript
airconsole.getUserMedia({ audio: true });
```

to request access to the users audio stream on the controller.

You can also request user media constraints for audio:

```javascript
airconsole.getUserMedia({
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true
  }
});
```

## Where It Works

After `airconsole.onReady` has fired, call `getUserMedia()` from a controller, not from the screen. Calling before `onReady`, before the controller has a device id, rejects `getUserMedia()` with `NotReady`.

The screen can learn whether one of the controllers was granted or denied microphone access through callbacks:

```javascript
airconsole.onUserMediaAccessGranted = function(deviceId) {
  console.log("Controller " + deviceId + " has microphone access");
};

airconsole.onUserMediaAccessDenied = function(deviceId) {
  console.log("Controller " + deviceId + " was denied microphone access");
};
```

The controller that requested access should use the returned Promise to know its own result:

```javascript
airconsole.getUserMedia({ audio: true })
  .then(function(stream) {
    // This controller got access.
  })
  .catch(function(error) {
    // This controller did not get access.
  });
```

## Handling Errors

`getUserMedia()` returns a Promise. It rejects when AirConsole cannot start the request or when the browser/app cannot grant microphone access.

AirConsole-specific errors extend `AirConsoleUserMediaError`&#x20;

```javascript
error.name === "AirConsole.UserMediaError"
```

Common AirConsole-specific messages:

- `NotSupportedOnScreen`: the screen called `getUserMedia()`.
- `NotReady`: AirConsole is not ready yet.
- `AlreadyPending`: another microphone request is already in progress on this controller.
- `InvalidConstraints`: the constraints are missing, false, or unsupported by AirConsole 1.11.0.
- `Timeout`: the AirConsole permission flow timed out.
- `PermissionDenied`: AirConsole/platform permission flow denied the request.

Browser-originated errors use the browser's own error names. Common ones are:

- `NotAllowedError`: the user, browser, page, iframe policy, or app settings denied microphone access.
- `NotFoundError`: no matching microphone was found.
- `NotReadableError`: the microphone exists, but the browser/OS could not open it.
- `OverconstrainedError`: your audio constraints could not be satisfied.
- `AbortError`: the browser could not use the device after starting the request.

A practical handler:

```javascript
function showMessage(message) {
  document.getElementById("microphone-status").textContent = message;
}

function handleMicrophoneError(error) {
  if (error instanceof AirConsoleUserMediaError) {
    if (error.message === AirConsole.USER_MEDIA_ERROR_TYPE.notSupportedOnScreen) {
      showMessage("Microphone input is only available on controllers.");
    } else if (error.message === AirConsole.USER_MEDIA_ERROR_TYPE.alreadyPending) {
      showMessage("Microphone permission is already being requested.");
    } else if (error.message === AirConsole.USER_MEDIA_ERROR_TYPE.timeout) {
      showMessage("Microphone permission timed out. Try again.");
    } else {
      showMessage("Microphone is not available right now.");
    }
    return;
  }

  if (error instanceof DOMException) {
    if (error.name === "NotAllowedError") {
      showMessage("Allow microphone access in your browser or app settings.");
    } else if (error.name === "NotFoundError") {
      showMessage("No microphone was found on this device.");
    } else if (error.name === "OverconstrainedError") {
      showMessage("This microphone does not support the requested audio settings.");
    } else {
      showMessage("Could not start the microphone.");
    }
    return;
  }

  showMessage("Could not start the microphone.");
}
```

## Recommended Game Flow

Ask for microphone access only when the player intentionally enables an audio feature. A button in the controller lobby is usually better than requesting immediately on page load  as certain browsers will reject permission requests without user interaction. If you use the `request-microphone` button from the quick-start HTML above, the flow is:

```javascript
airconsole.onReady = function() {
  document.getElementById("request-microphone").onclick = function() {
    airconsole.getUserMedia({ audio: true })
      .then(startMicrophoneGameplay)
      .catch(handleMicrophoneError);
  };
};
```

Keep the game playable when microphone access fails. For example, keep button controls available or let the player retry.

Do not call `getUserMedia()` repeatedly while one request is pending. AirConsole rejects concurrent requests with `AlreadyPending`.

:::hint{type="info"}
Do as much of the audio processing as possible on the controller. The message rate limit and the users network bandwidth can impact the users game experience otherwise.
:::



## Example: Send Audio Level To The Screen

This example measures microphone volume on the controller and sends a simple level value to the screen.

Controller HTML:

```html
<button id="request-microphone" type="button">Enable microphone</button>
<button id="stop-microphone" type="button">Stop microphone</button>
<p id="microphone-status"></p>
```

Controller JavaScript:

```javascript
var micStream = null;
var audioContext = null;
var analyser = null;
var animationFrame = null;
var lastSendTime = 0;
var source = null;

airconsole.onReady = function() {
  document.getElementById("request-microphone").onclick = startMicLevelInput;
  document.getElementById("stop-microphone").onclick = stopMicLevelInput;
};

function setMicrophoneStatus(message) {
  document.getElementById("microphone-status").textContent = message;
}

function startMicLevelInput() {
  airconsole.getUserMedia({ audio: true })
    .then(function(stream) {
      var AudioContextClass = window.AudioContext || window.webkitAudioContext;

      micStream = stream;
      audioContext = new AudioContextClass();
      source = audioContext.createMediaStreamSource(stream);

      analyser = audioContext.createAnalyser();
      analyser.fftSize = 256;
      source.connect(analyser);

      setMicrophoneStatus("Microphone enabled");
      sendMicLevelLoop();
    })
    .catch(handleMicrophoneError);
}

function sendMicLevelLoop() {
  var data = new Uint8Array(analyser.frequencyBinCount);

  function tick() {
    animationFrame = requestAnimationFrame(tick);

    var now = Date.now();
    // We want to send the information only 10 times per second of our 25 messages / second limit.
    if (now - lastSendTime < 100) {
      return;
    }
    lastSendTime = now;

    analyser.getByteFrequencyData(data);

    var sum = 0;
    for (var i = 0; i < data.length; i++) {
      sum += data[i] * data[i];
    }

    var level = Math.sqrt(sum / data.length) / 255;
    airconsole.message(AirConsole.SCREEN, {
      type: "audio-level",
      level: level
    });
  }

  tick();
}

function stopMicLevelInput() {
  if (animationFrame) {
    cancelAnimationFrame(animationFrame);
    animationFrame = null;
  }

  if (micStream) {
    micStream.getTracks().forEach(function(track) {
      track.stop();
    });
    micStream = null;
  }

  if (source) {
    source.disconnect();
    source = null;
  }

  if (analyser) {
    analyser.disconnect();
    analyser = null;
  }

  if (audioContext) {
    audioContext.close();
    audioContext = null;
  }

  setMicrophoneStatus("Microphone stopped");
}

function handleMicrophoneError(error) {
  setMicrophoneStatus("Microphone is not available");
  console.error("Microphone access failed:", error);
}
```

Screen:

```javascript
airconsole.onMessage = function(deviceId, data) {
  if (data && data.type === "audio-level") {
    var level = data.level;
    // Use level for gameplay or visualization.
  }
};
```

The example sends at most 10 audio-level messages per second. Keep audio telemetry small and rate-limited; games should not stream raw microphone data through AirConsole messages.

For a complete game structure, start from the AirConsole Pong example repository:

- [https://github.com/AirConsole/games-pong](https://github.com/AirConsole/games-pong)

In this game you will find `controller.html` which implements the getUserMedia requesting.

Additionally you will find `screen.html` that subscribes to the [`airconsole.onUserMediaPermissionGranted`](https://airconsole.github.io/airconsole-api/beta/AirConsole-Microphone_Permission.html#onUserMediaAccessGranted) and [`airconsole.onUserMediaPermissionDenied`](https://airconsole.github.io/airconsole-api/beta/AirConsole-Microphone_Permission.html#onUserMediaAccessDenied) callbacks.

Use those as full-context examples for where controller setup, `onReady`, lobby UI, screen messages, and microphone cleanup live in an actual game.

## Browser And App Behavior To Expect

Browsers and mobile apps do not expose microphone permission exactly the same way.

On web controllers:

- the browser may show its own microphone prompt.
- the Promise can stay pending if the player ignores the prompt.
- AirConsole rejects with `Timeout` after its own timeout period.
- browser failures such as `NotAllowedError` or `NotFoundError` may be returned.

On native AirConsole controller apps:

- the app may show native permission UI.
- if permission was denied permanently, the player may need to open system settings. AirConsole will inform the user about that.
- older app versions may not support the feature. AirConsole will in this case inform the user about the need to update the application.

Build your game UI around outcomes, not platform assumptions:

- granted: start audio gameplay.
- denied: When the user denies microphone access, the platform will be handling explain how to allow microphone access or offer a retry.

## Checklist

- Call `getUserMedia()` only from controllers.
- Request microphone with `{ audio: true }` unless you have a reason for advanced audio constraints.
- Handle both `AirConsole.UserMediaError` and browser error names.
- Keep a non-microphone fallback.
- Stop all tracks when leaving the audio feature, lobby, or game.
- Rate-limit any audio-derived messages sent to the screen.
- Do not request video through AirConsole 1.11.0.

## References

- MDN `MediaDevices.getUserMedia()`: [https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)
- MDN `MediaTrackConstraints`: [https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints)
- MDN `MediaStreamTrack.stop()`: [https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/stop](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/stop)

[title] Testing your game
[path] Getting started with AirConsole/

### *The Simulator*

The AirConsole simulator makes it easy for you to test your game. It provides a way to load your game into the AirConsole environment and shows you the screen and (on start) two controllers. With that you can easily play your game in one browser window without having to use another device, such as a smartphone.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/pOc9QdT5XALSjNQtZVAVn_image.png)

[http://airconsole.com/simulator/#debug\:YOUR\_HOST\_OR\_URL](http://airconsole.com/simulator/#debug\:YOUR_HOST_OR_URL)

Open Simulator Demo: [https://www.airconsole.com/simulator/#debug\:https://game.airconsole.com/com.airconsole.testpong.cdn.airconsole.com/2024-02-14-14-52-40/](https://www.airconsole.com/simulator/#debug\:https://game.airconsole.com/com.airconsole.testpong.cdn.airconsole.com/2024-02-14-14-52-40/)

**How to access the simulator**

The website on which your game runs has to be accessible - either by an IP or an URL. To load your game simply replace *YOUR\_IP\_OR\_URL&#x20;*&#x77;ith your games-location:
If you use a local ip, use:

:::BlockQuote
**http\://http.airconsole.com/simulator/#debug:***YOUR\_IP\_OR\_URL*
:::

If you use a https reachable game, use:

:::BlockQuote
**https\://www\.airconsole.com/simulator/#debug:***YOUR\_URL*
:::

For example the pong game's testing url is:

:::BlockQuote
**https\://www\.airconsole.com/simulator/#debug:**&#x68;ttps\://game.airconsole.com/com.airconsole.testpong.cdn.airconsole.com/2024-02-14-14-52-40/
:::

Or if hosted on localhost:

:::BlockQuote
**http\://http.airconsole.com/simulator/#debug:**&#x68;ttp\://192.168.0.1/
:::

:::BlockQuote
**NOTE:** AirConsole will try to access the /screen.html and a /controller.html, so make sure its accessible in the root url.
:::

**Join with your smartphone**

Of course you can additionally join with your smartphone. Just connect your phone like you would normally connect to a game: go to airconsole.com and enter the code which appears in the developer consoles top right.

:::BlockQuote
**NOTE:** If you are testing with real smartphones, make sure you do not use http\://localhost/ or http\://127.0.0.1/, because localhost is not your computer on the phone but the phone itself! Also make sure your devices are visible within the network to each other.
:::

### *Test https with ngrok*

Once your game is uploaded to the AirConsole store, it will be accessed via https. This means all your external resources you load in your game have to be loaded through https too.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/W2J9ekrJsp4I3v-Nusloh_image.png)

To simulate https locally we recommend to use [ngrok](https://ngrok.com/) or [cloudflare tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-local-tunnel/).



1. Go to [ngrok-website ](https://ngrok.com/)download and install ngrok. :)
2. (Optional for unix-based systems: Add to path: <font color="#eb144c">`sudo mv ngrok /usr/local/bin`</font>)
3. Execute ngrok in the terminal: <font color="#eb144c">`ngrok http 8000`</font>
   (Assuming 8000 is the port your local webserver is running at).
4. Ngrok will display some urls now -> copy the https url and try if it works in the simulator.

**Example**

Your game url on localhost is running at<font color="#eb144c">`192.10.20.42:8080`</font>and the URL from ngrok (<font color="#eb144c">`ngrok http 8080`</font>) is<font color="#eb144c">`https://cb501143.ngrok.io`</font>. To test your game in the simulator with https open:

:::BlockQuote
https\://www\.airconsole.com/simulator/#*https\://cb501143.ngrok.io/*
:::

### *Troubleshooting: Using Airconsole on a standard University/Corporate network*

This issue can also be solved by using [ngrok](https://ngrok.com/) (see above). This way you can make your localhost public even if there is client isolation.

:::BlockQuote
**Note to Unity Developers:** You need the latest airconsole unity plugin from [github.com](https://github.com/AirConsole/airconsole-unity-plugin) released on 2016-02-01 and you should select "Normal" as the browser start mode.
:::


[title] Introduction
[path] /

We have created the following documents to clarify how AirConsole system works and to help with the game integration into our platform.


[title] Tutorials
[path] /


[title] Game Language Translations
[path] AirConsole Games Features/Improving your game engage/

Translating your games to multiple languages broadens your audience.

### *1. Basics*

- Every displayed text in your game is a string (Graphics can not be translated).
- Every string has an ID. Example: The text "Welcome to our game" could have the id "<font color="#eb144c">`welcome_message`</font>".
- Every string has to be registered in AirConsole's translation console.
- You or AirConsole Translators can translate all your string in the translation console to any language
- You don't have to worry about language selection / loading language files.

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/1Cn-BBIA6JwwaiEfdGH0z_image.png" size="80" width="760" height="315" position="center" darkWidth="760" darkHeight="315" showCaption="false"}

### *2. AirConsole Translation console*

The translation console is the central repository for all your strings.
Any string that you want to have translated needs to be in there.

The translation console can be found in the developer console, inside one of your games under "Translations". You first need to register a game in the developer console before you can start adding translation string.

To add new translation strings, just follow the instructions in the translation console.

Once you have translation strings registered, you can select a language in the drop down and start to translate them. You can also add additional translators by email address to get some help with other languages. These translators can only suggest translations, but can not create or modify translation ids.

Anytime a translator or you changes a translation, you need to approve the suggestion before it goes to production. You can preview the changes with the "Preview" buttons.

:::BlockQuote
Local development always uses suggested translations and not live translations.
:::

[Go to Developers Console](http://www.airconsole.com/developers)

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/IXuANxwK3ya5rtbVzVLZd_image.png" size="98" width="932" height="739" position="center" darkWidth="932" darkHeight="739" showCaption="false"}

### *3. Using Translation Strings in your game*

To use translations, you need to call the AirConsole constructor with the option <font color="#eb144c">`translation: true`</font>

:::BlockQuote
new AirConsole(\{"translation": true})
:::

Once you have registered a string in the translation console, you can start using it inside your code. Assuming you have registered a string <font color="#eb144c">`welcome_screen`</font> that has the English value "Welcome to our game", you can retrieve it using:

:::BlockQuote
airconsole.getTranslation("welcome\_screen");
:::

If the users language is English, it will return "Welcome to our game".

For static html translations you can use the <font color="#eb144c">`data-translation`</font> attribute. This will automatically fill in your static translation:

:::BlockQuote
\<span data-translation="welcome\_screen">\</span>
:::

This will result in:

:::BlockQuote
\<span data-translation="welcome\_screen">Welcome to our game\</span>
:::

Translation strings can use replacement parts. For example <font color="#eb144c">`welcome_user`</font> could be have an English value of <font color="#eb144c">`Hi %username%`</font>. To retrieve the translated string with the username filled in, you can call:

:::BlockQuote
airconsole.getTranslation("welcome\_user", \{"username": "Tom"});
:::

which would result in "Hi Tom".

Replacement parts can only be used in programatically and can not be used with the static html attribute <font color="#eb144c">`data-translation`</font>.

You should never concatenate translation strings to build a sentence. Always use replacement parts instead. Different languages have different sentence structures.

:::BlockQuote
To preview a different language, add the "?language" parameter when loading AirConsole. e.g.: www\.airconsole.com/?language=de
:::

### ***4**. Advanced usage*

You can also use your own translation system. A quiz for example could load different questions depending on the current language. To get the current language, call:

:::BlockQuote
airconsole.getLanguage()
:::

This could also be abused to load different images depending on the users language and therefor you could use graphics containing texts.

:::BlockQuote
Note that the AirConsole Translation team will not be able to help you with your translations.
:::

### *5. Translations in Unity*

To enable translation functionality within your Unity project, do the following on your AirConsole Game Object:

- enable the <font color="#eb144c">`Translation`</font> checkbox.
- enter your game id.

You can see the translation functionality in action in the 'translations' example scene that you can find in your plugin under <font color="#eb144c">`Assets/AirConsole/examples/translations`</font>.

The example scene uses a few pre-defined translation strings set up for the game ID <font color="#eb144c">`com.airconsole.plugin.unity`</font>. You can request these for testing purposes, but you cannot edit them or add more.

:::BlockQuote
To test translations within the Unity Editor, you need to create an entry for your game in our [Developer Console.](http://www.airconsole.com/developers) This is where you define your Game ID.
:::

Request an individual translation string by id:

:::BlockQuote
string welcomeScreenTranslated = AirConsole.instance.GetTranslation("welcome\_screen");
:::

Request a translation with replacement parts. The values have to be passed along as a<font color="#eb144c">`Dictionary <string, string >`</font>:

:::BlockQuote
string welcomeUserTranslated = AirConsole.instance.GetTranslation("welcome\_user", new Dictionary \<string, string> \{ \{ "username", "Tom" } });
:::

You can translate multiple UI Text or Text Mesh elements (without replacement values). For it to work, the<font color="#eb144c">`text`</font>property of the UI Text or Text Mesh needs to contain the translation ID string within double curly brackets:<font color="#eb144c">`{{welcome_screen}}`</font>

:::BlockQuote
public Text\[] textsToTranslate;

&#x20;           void OnReady(string code) \{

&#x20;             AirConsole.instance.TranslateUIElements(textsToTranslate);

&#x20;           }
:::

:::BlockQuote
You cannot test different languages within the Unity Editor. Upload your Game to the [Developer Console](http://www.airconsole.com/developers) to enable and test different languages.
:::

### *6. Importing and exporting translations*

The importing/exporting translations functionality simplifies the process of working with changes on many string IDs, whether creating a new one or editing the existing ones.

:::BlockQuote
All translations are imported from or exported to the **.xlsx** file format.
:::

The import and export buttons can be found in the side menu.

**Importing Translations**

The import file must have the following structure:

- The first column represents translation IDs.
- Each other column represents one of the languages that you are importing.
- The header row for the translation IDs, which is the first column, must have the value **"string-id"**.
- The header row for each language column contains the value of the language code for the language being imported. You can find available languages and their corresponding codes below.
- For each row, the string ID name is in the first column, and each corresponding language column contains the value of the string ID in the imported language.

This is an example of the import file:

![import example](https://archbee-image-uploads.s3.amazonaws.com/DlW7QKqnO54P6fJk_G6J2-kFf6KCrOW6aCmHGgE0jE7-20250602-124256.png)

You can download a template here:

::File{src="https://archbee-doc-uploads.s3.amazonaws.com/2_r-qrqZUGzk_b42RwDvD-7DwchpupMy_7chRg-426Z-20241119-104144.xlsx" label="import_translations_template.xlsx"}

When translations are imported, the following rules apply:

- When importing translations with new string IDs, the new string IDs will be automatically registered with the default metadata:
  - Devices: Available on both
  - Description:
  - Render limit: No
- If you import a new string ID, the string ID value in the English column is mandatory.
- All translation string ID's new values in importing languages will be displayed in the translation console as new changes in the corresponding language section and have to be manually approved. Only English values of the new string IDs will be automatically approved as the part of the process of the registration of the new string IDs.
- A string ID with an empty value in a language column does not delete the previous value from the database for that language. It just skips the importing of the value for the language.
- The system's flexibility allows you to import new string IDs and edit the language values of the existing ones in the same import file.
- If the import file contains irregularities, error messages will be displayed addressing the problems. Please correct all errors and try importing again.

**Exporting Translations**

- All available translations for the game will be exported.
- The export file structure mirrors the pattern defined in the import translations section.
- The values of production translation string IDs are sorted in the columns by language, with the language code in the header row.
- New translation string values that have not been approved are also sorted in the columns per language, with the language code suffixed with "-dev" in the header row.

**Translation languages**

The AirConsole platform is currently translated into the following languages: Chinese, English, French, German, Indonesian, Italian, Japanese, Korean, Portuguese, Portuguese(Brazil), Spanish, Thai, and Ukrainian, but you can provide your game in many more languages.

[title] Handling unstable connections
[path] AirConsole Games Features/Connect to your game/

Your game needs to handle unstable connections and high latency. In this guide you will learn about properly handling unstable connections (Such as in cars, unstable 4G/Wi-Fi networks)

### *Internet Connections can be unstable (E.g. in cars)*

The AirConsole API makes sure that no messages and updates from the Screen and the Controllers are lost. A message or update sent by a device is guaranteed to arrive at some point in the right order even if the internet connection was disrupted, but the latency can be high.

### *Loading file resources in your game*

:::BlockQuote
You are responsible to reload resources if they fail to load after <font color="#eb144c">`onReady()`</font> is called.
:::

If a request fails before onReady is called in the API, we will reload the entire game. But once onReady was called, you have to reload resources that fail to load yourself. The best practice is to load all resources before onReady if you know that they are going to be used. Additional levels and big data files should still be loaded on demand at a later point, but you are responsible to handle load failures/retries.

### *Implementing onPause() / onResume() events*

**General**

:::BlockQuote
It is vital that you implement the <font color="#eb144c">`onPause()`</font> / <font color="#eb144c">`onResume()`</font> events.
:::

When a device such as a car's infotainment screen loses the internet connection (E.g. in a tunnel), the inputs from the Controllers will not arrive on time. When AirConsole detects a very high latency or temporary disconnect from the internet, the <font color="#eb144c">`onPause()`</font> method will be called, and you should immediately pause the game and all audio. You don't need to inform the user about pausing the game since AirConsole will provide a pause overlay on the screen and all controllers.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/XfgwoK6MksanOmTGlPN9-_image.png)

:::BlockQuote
Make sure that pause still works if called twice!
:::

Sometimes, there may be a delay in displaying the pause overlay on Controllers. This happens when the connection between the Controllers and the screen is completely lost, but as soon as the connection is re-established, the pause overlay will appear.

:::BlockQuote
Please, do not create any informative screens about pausing the game. AirConsole will provide pause overlay on Screen and all Controllers.
:::

Once the internet connection conditions are back to normal, AirConsole will call <font color="#eb144c">`onResume()`</font>, and you should resume the game and all sounds. AirConsole will show a countdown overlay for 3 seconds before <font color="#eb144c">`onResume`</font> event is called.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/DSDkmkD3yHpRehPYs0Ezu_image.png)

:::BlockQuote
The<font color="#eb144c">`onPause()`</font>method will only be called on the Screen! Controller input will still arrive on the Screen and should not change the game's state before<font color="#eb144c">`onResume()`</font>is called.
:::

<font color="#eb144c">`onPause()`</font> / <font color="#eb144c">`onResume()`</font> methods were added in AirConsole API 1.8.0. You can test them in the [AirConsole Simulator](http://www.airconsole.com/simulator/) if you are using at least API version 1.8.0.

**Testing**

Please include these following cases in your tests:

- **Test Case 1 - Initially (Game loaded)**

The Screen is disconnected from the internet while the game is loading and the <font color="#eb144c">`onPause()`</font> function is called. When the game loads, it calls the <font color="#eb144c">`onReady()`</font> callback function. AirConsole will call the <font color="#eb144c">`onPause()`</font> function right after it executes the <font color="#eb144c">`onReady()`</font> function from the game that just loaded. Make sure that this is handled properly and that you pause any game logic, intro videos or other background music immediately.

**Steps:**
Load your game preview in the AirConsole Simulator, e.g.: https\://www\.airconsole.com/simulator/#https\://game.airconsole.com/com.your.game/2023-06-07-16-06-19/ with **Pause on start** selected.

**Expected:**
The game should be paused and not producing any sounds (E.g. background music, intro sound/music, video intro, etc.)

- **Test Case 2 - During gameplay**

Disconnecting the Screen from the internet while playing the game should pause the game and all sounds in the game.

**Steps:**

- Load your game preview in the AirConsole Simulator, e.g.: https\://www\.airconsole.com/simulator/#https\://game.airconsole.com/com.your.game/2023-06-07-16-06-19/
- Press the pause button any time during the gameplay (E.g. While loading a game level, while in the game menu, while playing, etc.)

**Expected:**
The game should be paused and not producing any sounds (E.g. background music, intro sound/music, video intro, etc.)

- **Test Case 3 - Repetitive calls to**<font color="#eb144c">`onPause()`</font>

Calling the <font color="#eb144c">`onPause()`</font> function multiple times will not cause any issues.

The game should remain paused and not exhibit different behavior even if <font color="#eb144c">`onPause()`</font> is called repeatedly.

**Steps:**
Simulate calling the <font color="#eb144c">`onPause()`</font> function multiple times (without calling <font color="#eb144c">`onResume()`</font> in between).

**Expected:**
Invoking <font color="#eb144c">`onPause()`</font> on an already paused game should keep the game in a paused state.

- **Test Case 4 - Repetitive calls to**<font color="#eb144c">`onResume()`</font>

Calling the <font color="#eb144c">`onResume()`</font> function multiple times will not cause any issues.

The game should remain active and not exhibit different behavior even if <font color="#eb144c">`onResume()`</font> is called repeatedly.

**Steps:**
Simulate calling the <font color="#eb144c">`onResume()`</font> function multiple times (without calling <font color="#eb144c">`onPause()`</font> in between).

**Expected:**
Invoking <font color="#eb144c">`onResume()`</font> on an already active game should keep the game active. It should not pause the game or behave differently even if called multiple times.

### *High Latency handling*

**General**

Input latency from Controllers can be significantly higher in cars. The API call <font color="#eb144c">`getServerTime()`</font> provides a synchronized clock between the devices.

For example in a Quiz game, this timestamp can be sent from the Controller to the Screen. If Controllers have different latency, you can still determine the correct winner by looking at all timestamps received from the Controllers to compare the real user input times.

This can be applied to almost any game. You can determine the latency of a message by sending the <font color="#eb144c">`getServerTime()`</font> timestamp from the Controller to the Screen and compare it with the <font color="#eb144c">`getServerTime()`</font> on the Screen. Once you know how many milliseconds the message took, you can consider this in your input. (E.g. rewind the game by X milliseconds, add the input, fast-forward X milliseconds again).

**Testing**

The best way to simulate a high latency between the controller and the screen is to use a VPN. The Chrome Devtools Network option won't throttle the WebRTC connection, only the loading of the game assets.

[title] Create engaging games
[path] AirConsole Games Features/


[title] Hero Exclusive Content
[path] Generate income with your game/

With the “Premium” functions of the AirConsole API, it is possible to restrict certain content or features to AirConsole Hero subscribers, respectively sessions with at least one AirConsole Hero subscriber in them.

### *Why should I add Exclusive Content?*

[AirConsole Hero](https://www.airconsole.com/hero) is a premium subscription for the AirConsole platform, which is otherwise available for free. Premium subscriptions are the main monetizations strategy for AirConsole and its games.

The developer of every game profits from users that get an AirConsole Hero subscription while playing their game, so it is in their best interest to give the users some additional, game-specific incentive.

:::BlockQuote
See [our guide on Ads and AirConsole Hero](https://developers2.airconsole.com/airconsole-hero-and-revenue) for more details.
:::

### *How do I add Exclusive Content?*

There are several approaches to having Hero-Exclusive content in your game. In general, the content that is exclusive to paying users should offer more variety or more convenience, but not a mechanical advantage over fellow non-Hero players in the session.

- **Cosmetic Options**

Example: Exclusive Characters in [Silly World Series](https://www.airconsole.com/play/silly-world-series), only available to Hero subscribers themselves.

- **Additional Content**

Example: Exclusive Levels in [Castle Hustle](https://www.airconsole.com/play/castlehustle), available if at least one person in the session is a Hero subscriber.

- **Convenient Features**

Example: Team Color Selection in [Hotline](https://www.airconsole.com/play/party-games/hotline) or Character Selection in [The Neighborhood](https://www.airconsole.com/play/the-neighborhood), available if at least one person in the session is a Hero subscriber.

You can label Hero-exclusive content in your game with the AirConsole Hero Logo. Find our logo package [here.](https://drive.google.com/drive/folders/0BxEs-y1NpJ4zZ0VERG5NUm40X00?usp=sharing)

### *Implementation*

There are four calls in the AirConsole API that you can use to find out which and how many devices are premium as well as offer the players to get premium.

See the “Premium” section in the [AirConsole API](https://www.airconsole.com/api/airconsole-1.9.0.js) to view exactly what each of these is used for.

:::BlockQuote
While “AirConsole Hero” is the brand name of our premium subscription, the API itself only uses the generic term “premium”!
:::

### *A-B-Test for your own game!*

A-B-testing refers to trying two different versions at the same time with half of the audience and seeing which performs better.

It is impossible for us to compare conversion rates across games since these are influenced by a variety of factors. If you want to find the best way of offering premium-exclusive content in your game, we recommend you gather the relevant data yourself and see what works in your specific case.

:::BlockQuote
To make sure one player/group of players is always getting the same version, you can for example show version A or version B based on whether the screen’s UID is divisible by 2 or not. Gather this data using Google Analytics or Unity Analytics.
:::


[title] Quick Start
[path] Getting started with AirConsole/

For Unity games, please refer to our [Unity tutorial](https://developers2.airconsole.com/airconsole-unity-plugin).

### *General idea*

1. Create a new directory with two files screen.html and controller.html.



::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/L3gEEzW6h4GKdEukxKEmf-pYf4Y9aMZjhx0U9wJXcTL-20250303-115341.png" size="40" width="234" height="164" position="center" darkWidth="234" darkHeight="164" showCaption="false"}

2\. Include the [AirConsole JS API](https://aircn.sl/js-api) and use it to communicate between devices.

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/L3gEEzW6h4GKdEukxKEmf-aEidQrS8JiA8OvV8dbnLf-20250303-115406.png" size="40" width="239" height="164" position="center" darkWidth="239" darkHeight="164" showCaption="false"}

3\. Host your files (local/remote) and call: **airconsole.com/#**&#x68;ttp\://YOUR\_GAME\_URL/

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/L3gEEzW6h4GKdEukxKEmf-Eu2hHO4ILHwwZuLmSQzLe-20250303-115426.png" size="30" width="116" height="164" position="center" darkWidth="116" darkHeight="164" showCaption="false"}

### *Step by step guide*

**Create files:**

Create a new directory for your game (E.g. game) and create two files in it:

**/game**

- <font color="#eb144c">screen.html</font>
- <font color="#eb144c">controller.html</font>

The screen.html is for the gameconsole's screen.
The controller.html are for the gameconsole's controllers (gamepads).

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/L3gEEzW6h4GKdEukxKEmf-ApSCsHN9_ZLgLObHnPptK-20250303-115535.png" size="40" width="260" height="240" position="center" darkWidth="260" darkHeight="240" showCaption="false"}

:::hint{type="warning"}
Make sure both files can be accessed by: [http://your-local-webserver/screen.html](http://your-local-webserver/screen.html) (or /controller.html)
:::

If you don't want to create your own screen.html and controller.html right now and just play around, you can use our pong example [(pong.zip)](https://github.com/AirConsole/games-pong/archive/master.zip) from [the Classic Pong walkthrough](https://developers2.airconsole.com/pong-example).

**Include AirConsole Javascript API file:**

Include the AirConsole Javascript API in both files <font color="#eb144c">/screen.html</font> and <font color="#eb144c">/controller.html</font>

```html
<script type="text/javascript" src="https://www.airconsole.com/api/airconsole-1.11.0.js"></script>
```

**Send a message:**

Here is a simple example where the controller sends "*How are you?*" to the screen and the screen replies with "*Full of pixels!*"

<font color="#eb144c">/controller.html</font>

```javascript
 <button id="btn-msg">Send Message</button>

 <script type="text/javascript">
    var airconsole = new AirConsole();

    // When we press our button, we send a message to the screen
    document.getElementById('btn-msg').addEventListener("click", function() {
      airconsole.message(AirConsole.SCREEN, "How are you?");
    });

    // Listen for messages
    airconsole.onMessage = function(from, data) {

      // Show message on device screen
      var info = document.createElement('DIV');
      info.textContent = data;
      document.body.appendChild(info);
    };
</script>   
```

<font color="#eb144c">/screen.html</font>

```javascript
<script type="text/javascript">
    var airconsole = new AirConsole();

    // Listen for messages from other devices
    airconsole.onMessage = function(from, data) {

      // We receive a message -> Send message back to the device
      airconsole.message(from, "Full of pixels!");

      // Show message on device screen
      var info = document.createElement('DIV');
      info.textContent = data;
      document.body.appendChild(info);
    };
</script>
```

**Test your game:**

You need to start a local webserver in your /game folder to serve the <font color="#eb144c">screen.html</font> and <font color="#eb144c">controller.html</font>

In this example we will host the two files on:

:::BlockQuote
http\://192.168.0.1:8080/screen.html
http\://192.168.0.1:8080/controller.html
:::

To run the game, go with the browser that should act as the screen to:

[https://www.airconsole.com/#http://192.168.0.1:8080/](http://www.airconsole.com/#http://192.168.0.1:8080/)

:::hint{type="danger"}
NOTE: there is NO screen.html at the end!
:::

there are problems with certain browsers like chrome not allowing access like this with http and https mixed. Alternatively you can use:
[http://http.airconsole.com/?http=1\&#http://192.168.0.1:8080/](http://http.airconsole.com/?http=1\&#http://192.168.0.1:8080/)
or use the simulator like desribed below

Game controllers (your smartphone) can join as usual over [www.airconsole.com](https://www.airconsole.com).
Enter the code you see on the screen.&#x20;

When accessing the game with a separate device make sure the content (controller.html) is also accessible for that device under the url in the link ( in our example http\://192.168.0.1:8080/controller.html )

The devices should now exchange the messages.

**Simulator:**

If you append /simulator in the URL, then you will get a split view of the screen and two controllers:

[http://http.airconsole.com/simulator/#http://192.168.0.1:8080/](http://www.airconsole.com/simulator/#http://192.168.0.1:8080/)

This makes testing and developing a lot easier

:::hint{type="warning"}
&#x20;If you run into issue with HTTPS (mixed content issues), refer to the [tunneling guide](https://developers2.airconsole.com/testing-with-your-phone#0a3QQ).
:::

**Device Ids - Important warning:**

Make sure you do not assume that the controllers have device\_ids 1, 2, 3 ... We strongly recommend you read the [device ids and states](https://developers2.airconsole.com/device-ids-and-state) guide!

**Next steps:**

Checkout the guide about our [pong example](https://developers2.airconsole.com/pong-example) or the [API](https://airconsole.github.io/airconsole-api/) to learn more about how to build games for AirConsole.

To getting started with uploading your game to AirConsole, you can log in here: [https://www.airconsole.com/developers](https://www.airconsole.com/developers)

[title] Game Viability Judging Criteria
[path] Creating and publishing your game/

When we receive a game for AirConsole, we have a variety of criteria that determine whether the game will be launched in the store, and whether it is good enough to be featured.

### *Basic Requirements*

What your game needs in order to be published in the AirConsole web store.

- There are no major bugs. The game has been tested thoroughly across Android and iOS controllers and various desktop browsers.
- Your game passes the [AirConsole checklist](https://developers2.airconsole.com/airconsole-checklist).
- You have read our [Best Practices guide ](https://developers2.airconsole.com/best-practices)and adapted your game accordingly.
- You have read our guide on [Smartphone Controllers ](https://developers2.airconsole.com/smartphones-as-controllers)and follow the basic do’s and don’ts.
- Your game doesn’t infringe on copyright and is free of content that is deceptive, misleading, false or threatening.

### *Advanced Requirements*

What your game needs in order to be featured at the top of the AirConsole store or win one of our game development contests.

- Your game has a consistent and appealing visual style
- Your game has fitting sound effects and music
- Your game has replay value and is interesting to play for longer than a few minutes
- Your game’s controls are completely intuitive and make sense for the platform
- Your game is easily understandable or has a good tutorial

:::BlockQuote
If you send us a version of your game, we will gladly provide feedback and point out what is missing for it to be launched.
:::


[title] Multi-screen multiplayer
[path] AirConsole Games Features/Main Screen & Controller/

:::BlockQuote
Prerequisites: API 1.9.0 or Unity Plugin 2.5.0 or higher
:::

## *Key concepts explained*

- An environment, as provided by AirConsole for the screen device, is a limited environment that the current screen is part of.
- A multi-screen multiplayer session is an online multiplayer session where AirConsole screens act as players in an online multiplayer game.
- The primary screen is the screen responsible for all decisions in an ongoing multi-screen multiplayer session.
- Secondary screens are additional screens in a multi-screen multiplayer session that have no responsibility and ability to make decisions.
- The primary controller is what you receive from the API using **getMasterControllerDeviceId()**.
- The primary multi-screen controller is the master controller of the primary screen.
- An environment defines a grouping of multiple screens that share one or more characteristics.
- An identifier in an environment enables the screen to identify the current (physical) environment they are in, like a specific car.
- A partner in an environment enables screens to identify other screens in a similar environment, for example the same brand of car.

## *Multi-screen multiplayer: What is it and how does it help us to make more engaging games?*

### *What is a multi-screen multiplayer game?*

A multi-screen multiplayer game in AirConsole is an online multiplayer game that uses additional information to

- enable matchmaking to allow screens in a single physical environment (a single car with multiple screens for example) to play together.
- enable matchmaking to allow screens on the same general environment (cars of a brand, web) to play together with a similar experience across multiple physical environments.

These new capabilities make it possible to create multiplayer games suitable for in-car entertainment that provide a comparable, fair experience to the different players.

### *How can my players play together on multiple screens in the same environment / car?*

To enable games to implement environment based multi-screen multiplayer gaming, AirConsole provides the **screen device** with information on the *environment* it is part of. This environment contains the so called *environment identifier* (**id**) that identifies the environment the screen is as uniquely as possible.

An example of such an environment can be a single car with one or more screens, which all share the same **id**.

To ensure reliability of multi-screen multiplayer game implementations created today, AirConsole always provides an *environment* with a valid **id** and **partner**.
For screens that are not part of a specific physical environment, an *environment* with a generic **id** and **partner** is provided. Players in this case would play against other screens purely online without being in physical proximity to each other.

### *How can the environment identifier be used to enable multiple screens in the same environment to play together?*

After the *environment* with its **id** is obtained on the web or in Unity, the game can use this information to identify waiting games from other screens in the same physical environment.

Example usages of the **id** would be:

- In a solution like [Photon Fusion](https://www.photonengine.com/fusion), the environment id can be used as the SessionName in the StartGameArgs to either start or join the same game session of this particular car.
- In a solution like [Photon Realtime](https://www.photonengine.com/realtime), the environment id can be used as the room name to either create or join the same room of this particular car.

This way, the game can ensure that only players in the same physical environment can play together, while those without a specific physical environment can play.

It is in this context simplest to think of a screen as a **group of players** that in the multiplayer context act like a single player. One of the screens, we call it *primary screen*, would be the *primary screen* that would be responsible to handle the configuration of the multi-screen multiplayer session. The *primary multi-screen controller* of this *primary screen* is in that sense the *primary controller* for the whole session, while the main controller of the *secondary screen* must not have any special rights.

### *How can the partner be used to improve multiplayer games?*

After the *environment* with its **partner** is obtained on the web or in Unity, the game can use this information as a grouping parameter to provide match making between devices of the same platform or car brand.


- In a solution like [Photon Fusion](https://www.photonengine.com/fusion), the **partner&#x20;**&#x63;an be used in SessionProperty in the [Matchmaking API through StartGameArgs](https://doc.photonengine.com/fusion/current/manual/connection-and-matchmaking/matchmaking) to either start or join games running on the same partner platform.&#x20;
- In a solution like [Photon Realtime](https://www.photonengine.com/realtime), the **partner&#x20;**&#x63;an be used in [OpJoinRandomRoomParams](https://doc.photonengine.com/realtime/current/lobby-and-matchmaking/matchmaking-and-lobby#filtering-room-properties-in-join-random) to enable random match making to only take place against other screens on the same partner platform.

## *Best Practices*

Best practice for multiplayer on multiple screens in the same environment / same car:

1. Query the multiplayer platform to see if a room / session with the same *environment&#x20;***id&#x20;**&#x61;lready exists.
2. When the query result is received:
   1. If a corresponding room / session exists, the screen should offer the option to join the other screen
   2. If no corresponding room / session exists, the screen should offer the option to start a multiplayer game in the car.
      In this case the screen starting the multiplayer would need to wait for the second screen to join and confirm their readiness before the multiplayer round starts.

Best practice for using the environment identifier and partner in multiplayer games:

- Unless there is a strong reason against it, always use the *environment* **partner** as
  - a grouping parameter in room / session creation
  - as a filter in match making

Together these will ensure that players on the same platform or car brand with similar performance metrics play together.

- when multi-screen multiplayer in a single physical enviroment is supported
  - use the *environment* **id&#x20;**&#x61;s room / session name
  - use the *environment* **id&#x20;**&#x74;o query for pending games in the same physical environment and join them

Best practice for handling screens and master controllers

- Only a single controller must be considered the *primary multi-screen controller&#x20;*&#x6F;f all screens after multiple screens are in the same session.
- Only the *primary screen&#x20;*&#x6D;ust be able to configure and control the multi-screen multiplayer session.
- When the *primary screen* leaves the multi-screen multiplayer session
  - one of the *secondary screens&#x20;*&#x6E;eeds to be promoted to become the *primary screen.*
  - only the new new *primary screen&#x20;*&#x6D;ust have the ability to end the multi-screen multiplayer session to continue on a single screen.
    In consequence the *master controller&#x20;*&#x6F;f the new *primary screen&#x20;*&#x61;lso becomes the new *primary multi-screen controller.*

## *How to access the environment information*

### *On the web*

```javascript
var partner = undefined;
var environmentId = undefined;
if (airconsole.devices[AirConsole.SCREEN].hasOwnProperty("environment") {
  var environment = airconsole.devices[AirConsole.SCREEN].environment;
  environmentId = environment.id;
  partner = environment.partner;
}
```

:::BlockQuote
var environmentId = undefined;
var partner = undefined;
var airconsole = new AirConsole(\{});

// somewhen later for example as you reach the multiplayer matchmaking part of your game
if (airconsole.devices\[AirConsole.SCREEN] && airconsole.devices\[AirConsole.SCREEN].hasOwnProperty("environment")) \{
&#x20; var environment = airconsole.devices\[AirConsole.SCREEN].environment;
&#x20; environmentId = environment.id;
&#x20; partner = environment.partner;
}
:::

You also get this through the **onDeviceStateChange&#x20;**&#x65;vent, checking for the sending device\_id === AirConsole.SCREEN and then accessing **device\_state.environment.id**. Be aware that the initial device state update might not yet contain the information, so always check if **device\_state.hasOwnProperty("environment")**.

:::BlockQuote
var airconsole = new AirConsole(\{});
airconsole.onDeviceStateChange = handleOnDeviceStateChanged;

function handleOnDeviceStateChanged(device\_id, device\_state) \{
&#x20; if (device\_id === AirConsole.SCREEN && device\_state.hasOwnProperty("environment")) \{
&#x20;   const \{ environmentId, partner } = device\_state.environment;
&#x20;   // do something with the partner and environmentId or do something with device\_state.environment itself.
&#x20; }
}
:::

### *In Unity*

:::BlockQuote
JToken deviceData = AirConsole.instance.devices\[AirConsole.SCREEN];
JToken environment = deviceData?\["environment"];
string partner = environment?\["partner"]?.ToString();
string environmentId = environment?\["id"]?.ToString();
:::

You can also get it from the onDeviceStateChange event

:::BlockQuote
AirConsole.instance.onDeviceStateChange += HandleOnDeviceStateChange;

void HandleOnDeviceStateChange(int device\_id, JToken device\_data) \{
&#x20; JToken environment = device\_data?\["environment"];
&#x20; string partner = environment?\["partner"]?.ToString();
&#x20; string environmentId = environment?\["id"]?.ToString();
&#x20; // do something with the partner and environmentId or do something with environment itself.
}
:::


[title] Generate income with your game
[path] /


[title] Untitled
[path] Reference/


[title] Testing with your Phone
[path] AirConsole Games Features/Connect to your game/

If you are on a public wi-fi such as at a university, library or game jam site, you may run into trouble connecting your phone, getting stuck on the loading screen instead. You can work around this by using tool like Cloudflare Tunnels or the paid version of ngrok, which lets you expose local servers to the public internet over secure tunnels.

**When is this guide relevant to you?**

This will help you if you can open and play your game with virtual controllers in Unity, but are not able to connect an actual phone.
Most likely, your phone will remain stuck on the Loading screen with the AirConsole logo and three dots visible.

This workaround can also be used if your phone and your computer cannot be connected to the same WiFi for any reason, for example because your computer is connected to a hotspot coming from your phone itself.

### *Using Local IP Override to test easily*

To configure the public HTTPS address using the *Local IP Override* of your AirConsole component, you need at least v2.14 of the AirConsole Unity Plugin

1. Setup and run cloudflared to match your needs following the Install and run cloudflared guide below.
2. Select your AirConsole game object and set your *Browser Start Mode&#x20;*&#x74;o **Normal or Debug.**
3. Copy your public https URL into the *Local IP Override&#x20;*&#x66;ield.

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/Alm17QnKdjdhtiTTtWoNr_image.png)

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/kZoMSXe6Hg_E8jpLafVA0_image.png" size="70" width="438" height="331" position="center" showCaption="false"}

Start the play mode using *Edit > Play&#x20;*&#x6F;r press *Ctrl/Cmd + P*.

### *Install and run cloudflared*

**If you only need access to your Unity playmode:**

- Follow the installation instructions for the [Cloudflared Quick Tunnel.](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/)
- Start it using
  `cloudflared tunnel --url http://localhost:7842`
  (the webserver port configured in your AirConsole settings)
- When running the cloudflared quick tunnel, you will receive a randomized URL like the one shown below to copy into the **Local IP Override:**

![](https://archbee-image-uploads.s3.amazonaws.com/eODGJqzvP6Walum1Gn5N9/KdmtXQN-yFs_hp35D1McY_image.png)

:::BlockQuote
Every time you run *cloudflared tunnel --url http\://localhost:7842* the resulting URL will change and need to be copied into the **Local IP Override** again.
:::

**If you need access to multiple ports including Unity playmode for your testing**

- Follow the installation instructions at [Create CloudFlare Local Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-local-tunnel/)
  - In the config.yaml, replace *url: http\://localhost:8000&#x20;*&#x77;ith *url: http\://localhost:7842*
- Insert the url you configured in config.yaml into the Local IP Override field. This will not change between executions of cloudflared.


[title] Device Ids and state
[path] Getting started with AirConsole/

It's important to know that in AirConsole controllers can join and leave at any time.

Some developers have wasted a lot of hours debugging, because they did not understand the following points.

### *Device IDs*

Every device in a AirConsole session has a device\_id.

The screen always has device\_id 0. You can use the <font color="#eb144c">`AirConsole.SCREEN`</font> constant instead of <font color="#eb144c">0</font>.

All controllers also get a device\_id.

:::BlockQuote
<font color="#eb144c">Do not hardcode controller device_ids! You can NOT assume that the device_ids of controllers are consecutive or that they start at 1.</font>
:::

Within an AirConsole session, devices keep the same device\_id when they disconnect and reconnect. Different controllers will never get the same device\_id in a session. Every device\_id remains reserved for the device that originally got it.

<font color="#eb144c">`airconsole.getControllerDeviceIDs();`</font> returns all currently connected controllers that have loaded your game.

### *setActivePlayers to the rescue!*

(AirConsole API 1.3.0 and above)

Devices can join and leave at any point in time during an AirConsole Session.
When you're game is loaded, maybe only controller device\_ids <font color="#eb144c">`[3, 7, 12]`</font> are still present and NOT <font color="#eb144c">`[1, 2, 3]`</font>.

This is where <font color="#eb144c">`setActivePlayers()`</font> comes into play. You don't have to use this helper function, but this mechanism is very convenient if you want to know which device is the first player, the second player, the third player ...
 <font color="#eb144c">`airconsole.setActivePlayers()`</font> takes all currently connected controllers and assigns them a player number. The assigned player numbers always start with 0 and are consecutive. You can hardcode player numbers, but not device\_ids. Once the screen has called <font color="#eb144c">`airconsole.setActivePlayers()`</font>, you can get the device\_ids of players by calling:

- <font color="#eb144c">`airconsole.convertPlayerNumberToDeviceId(0)`</font> for the first player
- <font color="#eb144c">`airconsole.convertPlayerNumberToDeviceId(1)`</font> for the second player
- <font color="#eb144c">`airconsole.convertPlayerNumberToDeviceId(2)`</font> for the third player
- ...

You can also convert device\_ids to player numbers by calling <font color="#eb144c">`airconsole.convertDeviceIdToPlayerNumber(device_id)`</font>. You can get all device\_ids that are active players by calling <font color="#eb144c">`airconsole.getActivePlayerDeviceIds()`</font>.

The screen can call this function every time a game round starts.

:::BlockQuote
In the beginning of a round in your game the screen can call <font color="#eb144c">`airconsole.setActivePlayers()`</font>. Then you can easily send a message to the first player by calling <font color="#eb144c">`airconsole.message(airconsole.convertPlayerNumberToDeviceId(0), "Hello first player!")`</font>
:::

### *Custom Device States*

Custom Device States are really powerful!

<font color="#eb144c">`airconsole.setCustomDeviceState(custom_data)`</font> sets data for a device, readable by all other devices at any point in time. <font color="#eb144c">`airconsole.onCustomDeviceStateChange(device_id, custom_data)`</font> will be called on all other devices, when data is set for a device - **even if a device joins at a later point** and was not connected when the data was set.

:::BlockQuote
A typical use case for custom device states is to control what "view" is displayed on the controller. Check out [https://github.com/AirConsole/airconsole-view-manager](https://github.com/AirConsole/airconsole-view-manager) if you want to do this.
:::

<font color="#eb144c">`airconsole.setCustomDeviceStateProperty(key, value)`</font>sets a single property in the custom device state, instead of overwriting the whole object.

<font color="#eb144c">`airconsole.getCustomDeviceState(device_id)`</font>gives you access to any devices custom device state at any point in time.

With Custom Device States you do not have to send messages to devices that connect after the games has already started with information about "state" of the game.

**A Custom Device State example**

If a player chooses to be the color blue, he sets his custom device state with <font color="#eb144c">`airconsole.setCustomDeviceStateProperty("my_color", "blue")`</font>.

All present devices will get notified with <font color="#eb144c">`airconsole.onCustomDeviceStateChange(device_id, {"my_color": "blue", [...]})`</font>.

If a new device joins later it will also get notified with <font color="#eb144c">`airconsole.onCustomDeviceStateChange(device_id, {"my_color": "blue", [...]})`</font>right after <font color="#eb144c">`onReady`</font>gets called.

[title] Best practices
[path] Creating and publishing your game/

Try the [AirConsole Checklist](https://developers2.airconsole.com/airconsole-checklist) before submitting your game!

### *General*

1. Try to test your game on as many as different phones and screens as possible.
2. Make sure all resources (external JS, CSS files etc.) can be loaded over https.
3. Games shouldn't request a separate login.
4. Having problems during development? Check the [airconsole tag on StackOverflow ](http://stackoverflow.com/questions/tagged/airconsole)- somebody might have had the same issue before.

### *Controller - Technical*

- Use <font color="#eb144c">`<div>`</font> tags with a background-image style as buttons instead of <font color="#eb144c">`<input>`</font> or <font color="#eb144c">`<img>`</font> tags.
- Use <font color="#eb144c">`touchstart`</font>and <font color="#eb144c">`touchend`</font>for event handlers instead of <font color="#eb144c">`click`</font>or <font color="#eb144c">`mousedown`</font>.

:::BlockQuote
var button = document.getElementById('button\_1');
button.addEventListener('touchstart', touchdownHandler);
button.addEventListener('touchend', touchendHandler);
:::

Or try the [airconsole-controls](https://github.com/AirConsole/airconsole-controls) library, which already takes care of this.

- Preload your images in the beginning

:::BlockQuote
// For example in javascript:
var images = \['button\_1.png', 'button\_2.png', 'character\_1.png'];
for (var i=0; i \< images.length; i++) \{
&#x20; var img = new Image();
&#x20; img.src = 'assets/images' + images\[i];
}
:::

### *Controller - Design*

- Always indicate which ingame-character the player is controlling.

:::BlockQuote
E.g. If the player is controlling the blue character in the game, then make the controller interface blue.
:::

- Avoid having unnecessary buttons

:::BlockQuote
E.g. you can just go left and right, so you don’t need a 4-directional-dpad
:::

- Show different states on the phone instead of always showing the ingame controller.

:::BlockQuote
If a player dies in the game and the game is still running, then show a “You are dead” view on the players controller.
:::

- Make sure the user-flow works. From Menu -> Game -> Game End -> Menu

### *Screen / Game*

- Explain your game to your players.

:::BlockQuote
E.g. with an intro-screen or a tutorial. Make sure that it's not too easy to skip the instruction screen by accident.
:::

- Check what happens if a player joins or leaves during a running game.
- If your game needs more than 1 Player, display it in the start screen.
- If your game has a max player amount, display a “max player reached” if too many players connected.
- Do not use device ids as player numbers. Device ids may not be consecutive, there may be devices that were connected but have left. [Read more](https://developers2.airconsole.com/device-ids-and-state)
- Instead of showing "Player 1", try to show the real player name (<font color="#eb144c">`(airconsole.getNickname()`</font>).
- Be aware that players may have a high latency.

### *Unity*

- Export your game early and often! Some issues may not appear in the editor and cause unpleasant surprises if you export it for the first time right before launch time.
- Images and JavaScripts need to be in <font color="#eb144c">`WebGLTemplates/AirConsole`</font>so your screen and controller can access them in the exported build.
- Use the Profiler to pinpoint performance issues and optimize them.
- Read up on the limitations of WebGL early enough so you don’t spend time on features that can’t be used in the exported version.
- Having memory issues? Can you reduce the number of GameObjects in your game, for example? If not, consider increasing the memory size in Build Settings -> Publishing Settings.
- Avoid Exceptions at all costs, because they will cause crashes in the WebGL export. If you’re getting an exception and don’t know why, make a debug build with Build Settings -> Publishing Settings -> Enable Exceptions set to “Full” and then look at the Console in your browser to see a precise error message.


[title] Smartphones as Controllers
[path] AirConsole Games Features/Main Screen & Controller/

The smartphone controllers are simultaneously one of the biggest challenges in developing games for AirConsole and also the aspect with the most potential for true innovation.

When developing an AirConsole game, the smartphone controls should be an integral part of early gameplay prototyping, since some mechanics that work with a gamepad will often feel subpar when controlled with a smartphone.

While we are proud of the over 100 games the AirConsole store currently offers, we believe that we have barely scratched the surface of what is possible in regards to innovative, intuitive and fun smartphone controlled local multiplayer games.

### *Main Takeaways*

- Use different controller views / layouts for different situations
- Label buttons / interactive areas with their actual function
- Make all buttons / interactive areas as big as possible
- Make use of personalized controllers for individual players
- Do not use d-pads and virtual joysticks
- Play existing AirConsole games to see which control schemes feel good and which don’t\*

:::BlockQuote
\*Feel free to ask the AirConsole team or our [developer community on Discord](https://discordapp.com/invite/5BzND3v) for recommendations for a specific type of game or control scheme you are interested in.
:::

### *Smartphones are not gamepads*

When designing a smartphone controller layout, the first and most important thing to realize and remember is that a smartphone screen is not a regular gamepad / xbox controller and should not be treated as such.

This provides some challenges, but also has its advantages:

**Pro: Individual Layouts**

The controller screen can be adjusted to meet any ingame situation and mechanic it needs to: whether you need it to only display a single big button or you want to let the player play an entire minigame on it.

Instead of having buttons (or other interactive areas) labelled with “A”, “B” or “X”, you can label all touch areas directly with their function and don’t need to include captions for them on the main screen.

**Pro: Personalized Information**

Every individual player can have their own controller view. This can be used for something as simple as displaying the player color, but you can also convey secret information (cards, items, secret objectives) or design asymmetrical input schemes where different players have different roles and input options.

**Pro: Input Variety**

Virtual buttons are not the only way to register input from a smartphone. Besides button taps, your game input can also use swiping and the phone's gyroscope, or even its camera, speakers or microphone. Everything that works in a mobile browser is possible on AirConsole.

**Con: No Tactile Feedback**

The most significant disadvantage smartphone controllers have over regular gamepads is the lack of tactile feedback: A player’s finger may slip over the screen while they are not looking down, and the player might not realize that their finger is no longer on the button or interactive area, which leads to frustration.

To combat this, interactive areas / buttons on AirConsole controllers should be made as big as possible, reducing the chance of players tapping empty space and growing frustrated.

### *Views*

**What are views?**

What we call views are different controller layouts for different ingame situations.

In the pictures below, you can see the “Ingame” and “Pause” views for [Dust Squad - Tanks For Nothing](https://www.airconsole.com/play/dust-squad).

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/L3gEEzW6h4GKdEukxKEmf-_g5ryvfDApdrbIknl0c-X-20250304-085105.png" size="70" width="562" height="357" position="center" darkWidth="562" darkHeight="357" showCaption="false"}

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/L3gEEzW6h4GKdEukxKEmf-22ZjglfQCph63Jnwh00DP-20250304-085124.png" size="70" width="565" height="359" position="center" darkWidth="565" darkHeight="359" showCaption="false"}

Views can be updated for more specific and/or frequently changing situations too. Three examples from Dust Squad where the view shows that

1. the player’s weapon is currently on cooldown
2. the player is close to Hero Exclusive content and can buy a subscription here
3. the player has died and is waiting for respawn

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/L3gEEzW6h4GKdEukxKEmf-rzaz7j0419wImrlEj1dPm-20250304-090027.png" size="70" width="1133" height="721" position="center" darkWidth="1133" darkHeight="721" showCaption="false"}

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/L3gEEzW6h4GKdEukxKEmf-mqWdmmYuezbsgDjVzjk9d-20250304-090050.png" size="70" width="565" height="359" position="center" darkWidth="565" darkHeight="359" showCaption="false"}

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/L3gEEzW6h4GKdEukxKEmf-JJ-Yw3b5qdNZKN-jeaggf-20250304-090108.png" size="70" width="564" height="358" position="center" darkWidth="564" darkHeight="358" showCaption="false"}

**How do I change views?**

We leave the exact implementation to you, but we recommend using Custom Device States and not messages to change your views.

We also recommend using the [AirConsole View Manager](https://github.com/AirConsole/airconsole-view-manager). With it you can add the ‘view’ class to div containers and easily show one of them and hide all others.

**Using Screen Estate**

Input fields (buttons, swipe areas etc.) should always be as big as possible, so that they are hard to miss. Reduce the number of different input fields by only showing the ones that are currently needed and updating the controller view whenever necessary.

*Bad:*

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/L3gEEzW6h4GKdEukxKEmf-R502aMAjutnAlz5ERJHwk-20250304-085902.jpg" size="70" width="800" height="450" position="center" darkWidth="800" darkHeight="450" showCaption="false"}

*Good:*

::Image[]{src="https://archbee-image-uploads.s3.amazonaws.com/L3gEEzW6h4GKdEukxKEmf-7crbyinKbSX8ghp2bEGAm-20250304-085929.jpg" size="70" width="800" height="450" position="center" darkWidth="800" darkHeight="450" showCaption="false"}

### *Visuals*

The controller’s visual design should fit the ingame graphics and big screen UI, adding up to an aesthetically pleasing whole picture.

When it comes to developing a UI art style with smartphone controllers in mind, it is helpful to use elements that can be stretched instead of relying on exact angles or aspect ratios. This can make the implementation for different phone screen resolutions with HTML/css easier.

### *Joysticks & D-Pads*

Virtual joysticks and movement D-Pads ***do not work*** for AirConsole. We have several games that use them, but free character movement with analogue sticks or directional buttons will ***always*** feel subpar on a smartphone controller in comparison to a gamepad.

This means that you cannot rely on including “character movement” in your game design without giving serious thought to how exactly it will work with a smartphone.

How character movement should work instead depends heavily on the game and there is no universal answer for how to solve this issue. This limitation should be considered in your game design from the very beginning.

### *Haptic Feedback*

Give you players more control over their actions by letting them feel what they are doing with haptic feedbacks.&#x20;

Be it direct feedback of the controls just on the controller side or vibrations triggered by ingame events happening on the screen.

The controller app of AirConsole for iOS and Android gives you more control over vibrations on the controller devices. With the [vibrate](https://airconsole.github.io/airconsole-api/AirConsole-Controller_Inputs.html#vibrate) function of the AirConsole API you can play several vibration primitives and chain them together to form a more complex pattern.

Use the Composition vibration interface to play vibration primitives by passing the VibrationOptions to the vibrate function:

:::BlockQuote
airconsole.vibrate(\{
&#x20;   type: AirConsole.VIBRATE.TYPE.COMPOSITION,
&#x20;   value: \[
&#x20;       \{
&#x20;           primitive: AirConsole.VIBRATE.PRIMITIVE.THUD,
&#x20;           scale: 0.8,
&#x20;       }
&#x20;   ]
})
:::

The above code is playing a vibration pattern that emulates a thud with a strength of `0.8`.

You can find all the available primitives in our API documentation.

To be able to create more complex pattern you can chain two or more primitives together to create a new pattern that can be executed with one call.

:::BlockQuote
airconsole.vibrate(\{
&#x20;   type: AirConsole.VIBRATE.TYPE.COMPOSITION,
&#x20;   value: \[
&#x20;       \{
&#x20;           primitive: AirConsole.VIBRATE.PRIMITIVE.QUICK\_FALL,
&#x20;           scale: 0.8,
&#x20;       },
&#x20;       \{
&#x20;           primitive: AirConsole.VIBRATE.PRIMITIVE.TICK,
&#x20;           scale: 0.9,
&#x20;           delay: 10,
&#x20;       },
&#x20;   ]
})
:::

The example above plays a quick fall vibration followed by a short tick vibration effect with a delay of 10ms after the quick fall has completed.

Please note that executing vibrations also drains the battery. Also make sure that you don't over do it an numb you players hands.

[title] How we achieve low latency
[path] AirConsole Games Features/Connect to your game/


[title] Phaser 2 Game Example
[path] Archive/

