|
| 1 | +Nostr Game Engine supports **native in-game advertising** through its integration with the [nostrads](https://nostr-ads.ngengine.org/) protocol. |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +## Preparing the App |
| 6 | + |
| 7 | + |
| 8 | +Before enabling ads, make sure your application is initialized with a valid `appId` (see [NGEApplication](../getting-started/#ngeapplication)). |
| 9 | + |
| 10 | +The `appId` must correspond to a pubkey that has a valid metadata event containing a **lnurl** or **lightning address** (`lud06` or `lud16`) for receiving ad revenue. |
| 11 | + |
| 12 | +The easiest way to set this up is by using a Nostr client such as [Primal](https://primal.net/). In your profile settings, you’ll find a field where you can set your lightning address: |
| 13 | + |
| 14 | + |
| 15 | + |
| 16 | +If you don’t yet have a lightning address for your app, here are some options (from simplest to more advanced): |
| 17 | + |
| 18 | +1. [Blink.sv](https://www.blink.sv/) - custodial mobile wallet that provides a lightning address. |
| 19 | +2. [Rizful](https://rizful.com/) - offers both custodial and non-custodial wallets with lightning addresses. |
| 20 | +3. [AlbyHub](https://albyhub.com/) - self-hosted non-custodial lightning node with a lightning address. |
| 21 | +4. [LNbits](https://lnbits.com/) - self-hosted interface that connects to various Lightning backends and can provide a lightning address via extensions. |
| 22 | + |
| 23 | + |
| 24 | +## Enabling Ads |
| 25 | + |
| 26 | +To enable ads in your app, call the `enableAds` convenience method on `NGEApplication`: |
| 27 | + |
| 28 | +```java |
| 29 | +NGEAppRunner appRunner = NGEApplication.createApp(appId, settings, app -> { |
| 30 | + // ... callback once app is ready .... |
| 31 | + app.enableAds(); |
| 32 | + // ... |
| 33 | +}); |
| 34 | +``` |
| 35 | + |
| 36 | +This will automatically: |
| 37 | + |
| 38 | +* Add an `ImmersiveAdComponent` |
| 39 | +* Initialize it with a default set of relays |
| 40 | +* Create a random **advertisement key** (an anonymous private key used to identify the current player in the ads network) |
| 41 | + |
| 42 | +If you want to use a custom set of relays and/or a custom advertisement key, you can pass them explicitly: |
| 43 | + |
| 44 | +```java |
| 45 | +NostrPrivateKey adsKey = NostrPrivateKey.generate(); |
| 46 | +app.enableAds(adsKey, List.of("wss://relay.ngengine.org", "wss://relay2.ngengine.org")); |
| 47 | +``` |
| 48 | + |
| 49 | +!!! note |
| 50 | + How you generate the `adsKey` is up to you. |
| 51 | + |
| 52 | + - You may generate a new random key for each play session. |
| 53 | + - Or you may persist it across sessions. |
| 54 | + |
| 55 | + |
| 56 | + In either case, **never link it directly to the player’s identity**, to preserve privacy. |
| 57 | + |
| 58 | + |
| 59 | + |
| 60 | + |
| 61 | +## Setting Up the Scene for Ad Spaces |
| 62 | + |
| 63 | +Next, you need to prepare your scene to detect ad spaces and display ads. |
| 64 | +This is done by adding an `ImmersiveAdControl` to the `rootNode` (or any other spatial in your scene): |
| 65 | + |
| 66 | +```java |
| 67 | +ImmersiveAdControl adControl = new ImmersiveAdControl(assetManager); |
| 68 | +map.addControl(adControl); |
| 69 | +``` |
| 70 | + |
| 71 | +Once added, register the control with the `ImmersiveAdComponent`: |
| 72 | + |
| 73 | +```java |
| 74 | +componentManager.getComponent(ImmersiveAdComponent.class).register(adControl); |
| 75 | +``` |
| 76 | + |
| 77 | +--- |
| 78 | + |
| 79 | +### Context-Aware Advertising and Ad Groups |
| 80 | + |
| 81 | +The setup above is enough to start showing ads, but it won’t filter them by context or preferences. |
| 82 | +For that, use the extended `ImmersiveAdControl` constructor: |
| 83 | + |
| 84 | +```java |
| 85 | +public ImmersiveAdControl( |
| 86 | + @Nonnull AssetManager assetManager, |
| 87 | + @Nullable List<AdTaxonomy.Term> categoryIds, |
| 88 | + @Nullable List<String> languages, |
| 89 | + @Nullable AdPriceSlot priceSlot, |
| 90 | + @Nullable String context // unused for now |
| 91 | +) {} |
| 92 | +``` |
| 93 | + |
| 94 | +!!! tip |
| 95 | + You can obtain `AdTaxonomy.Term` instances either by: |
| 96 | + |
| 97 | + **Creating a new taxonomy instance**: |
| 98 | + ```java |
| 99 | + AdTaxonomy taxonomy = new AdTaxonomy(); |
| 100 | + ``` |
| 101 | + |
| 102 | + **Or retrieving it from the component**: |
| 103 | + ```java |
| 104 | + AdTaxonomy taxonomy = componentManager |
| 105 | + .getComponent(ImmersiveAdComponent.class) |
| 106 | + .getTaxonomy(); |
| 107 | + ``` |
| 108 | + |
| 109 | + Then, fetch terms by ID or path: |
| 110 | + |
| 111 | + ```java |
| 112 | + Term term = taxonomy.getById("150"); |
| 113 | + Term term = taxonomy.getByPath("Attractions"); |
| 114 | + ``` |
| 115 | + |
| 116 | + For a full list of taxonomy IDs and paths, see the [Nostr Content Taxonomy CSV](https://ngengine.org/docs/nip-drafts/nostr-content-taxonomy/). |
| 117 | + |
| 118 | + |
| 119 | +#### Multiple Contexts in the Game World |
| 120 | + |
| 121 | +An `ImmersiveAdControl` only affects the spatial it’s attached to and its subtree. |
| 122 | + |
| 123 | +* If attached to the `rootNode`, it applies to the entire scene. |
| 124 | +* You can attach multiple controls to different children of the `rootNode`, each with different filters, allowing different ads in different areas of your game world. |
| 125 | + |
| 126 | +--- |
| 127 | + |
| 128 | +#### Advanced Filtering |
| 129 | + |
| 130 | +The `ImmersiveAdControl` also supports a custom filter via: |
| 131 | + |
| 132 | +```java |
| 133 | +adControl.setFilter((AdBidEvent event) -> { |
| 134 | + // return true to accept, false to reject |
| 135 | +}); |
| 136 | +``` |
| 137 | + |
| 138 | +This lets you implement complex **client-side filtering** logic, tailored to your app’s needs. |
| 139 | + |
| 140 | + |
| 141 | + |
| 142 | +## Adding Adspaces to 3D Models |
| 143 | + |
| 144 | +**Adspaces** are 3D surfaces in your game world where ads can be displayed. |
| 145 | + |
| 146 | +The `ImmersiveAdControl` automatically detects adspaces, applies the correct material, and loads ads as textures. |
| 147 | +However, you still need to **define the geometry** that will host each adspace. |
| 148 | + |
| 149 | + |
| 150 | +### Defining Adspaces Programmatically |
| 151 | + |
| 152 | +You can create adspaces directly in code by adding a `Geometry` of any shape (commonly `Quad` or `Box`) and tagging it with the supported resolution: |
| 153 | + |
| 154 | +```java |
| 155 | +Geometry adspace = new Geometry("AdQuad", new Quad(2, 4)); |
| 156 | +adspace.setUserData("nostrads.adspace", "256x512"); |
| 157 | +``` |
| 158 | + |
| 159 | +Here, `"256x512"` is one of the supported resolutions defined by the nostrads protocol. |
| 160 | +A full list of valid sizes can be found in the [nostrads documentation](http://localhost:8000/docs/nip-drafts/nip-ADS/#ad-size-and-aspect-ratio) (`s` tag). |
| 161 | + |
| 162 | +!!! note |
| 163 | + You must ensure the geometry’s **aspect ratio** matches the chosen ad size. |
| 164 | + Otherwise, the ad will appear stretched or distorted. |
| 165 | + |
| 166 | + |
| 167 | +### Defining Adspaces in a 3D Editor (Recommended) |
| 168 | + |
| 169 | +The preferred approach is to define adspaces directly in your 3D modeling tool (e.g. **Blender**). |
| 170 | + |
| 171 | +1. Create a geometry (e.g. a cube or plane) with the correct aspect ratio. |
| 172 | +2. Add the custom property: |
| 173 | + - Key: `nostrads.adspace` |
| 174 | + - Value: one of the supported resolutions (e.g. `256x512`). |
| 175 | + |
| 176 | +Resolutions must match the supported sizes defined by the nostrads protocol, in the [nostrads documentation](http://localhost:8000/docs/nip-drafts/nip-ADS/#ad-size-and-aspect-ratio) (`s` tag). |
| 177 | + |
| 178 | +!!! tip |
| 179 | + In Blender, when exporting to **glTF**, make sure to enable: |
| 180 | + `Include → Custom Properties` |
| 181 | + This ensures your `nostrads.adspace` property is exported correctly. |
| 182 | + |
0 commit comments