Skip to main content

Asteroids

A multiplayer game with projectiles and primitive enemies. This is an extreme case of Dynamic symbols usage. All particles are calculated on server and then, broadcasted every 1/30 sec. for all connected clients.

To run this demo

git clone https://github.com/JerryI/wl-wlx
cd wl-wlx
wolframscript -f Examples/Asteroids/Asteroids.wls

All players connection are managed by events system and a global event object "game"

players = <||>;
EventHandler["game", {
"Add player" -> Function[data,
Echo["Add player from socket"];
With[{cli = data["Client"]},
players[cli] = data;
]
],

"Remove player" -> Function[data,
Echo["Remove player"];
With[{cli = data["Client"]},
players[cli] = .;
]
],

_ -> Null
}];

App instances

For each window a separate instance of app is generated

App[OptionsPattern[]] := With[{
globalControls = OptionValue["Controls"],
localControls = CreateUUID[]
},
With[{
PlottingDevice = create[globalControls]
},

<div class="divide-y divide-gray-200 max-w-lg overflow-hidden rounded-lg bg-white shadow">
<div class="px-2 py-2">
<WLJS>
<PlottingDevice/>
</WLJS>
</div>
</div>
]
]

where create allocates symbols for players targets, life meter and etc...

create[controls_] := Module[{
target = {0,-1},
life = 2.,
score = 0,
listener,
enemies = {},
projectiles = {},
positions = {},
gameEvents,
delay = 5
},

gameEvents = EventClone["game"];
EventHandler[controls, {
"Destroy" -> Function[cli,
Echo["Widget removed!"];
EventFire["game", "Remove player", <|"Client"->cli|>];
EventRemove[gameEvents];
checkTask;
],

"Connected" -> Function[cli,
EventHandler[gameEvents, {
UpdatePositions -> Function[Null,
positions = #["Target"] &/@ Values[players];
],

UpdateProjectiles -> Function[Null,
projectiles = proximitySort[projectiles, #[[1]] &/@ globalProjectiles];
],

UpdateEnemies -> Function[Null,
enemies = proximitySort[enemies, #[[1]] &/@ globalEnemies];
If[life < 0,
Close[cli];
];
]
}];

EventFire["game", "Add player", <|"Client"->cli, "Target":>target, "Life":>life|>];
checkTask;
]
}];


listener = {White, EventHandler[Rectangle[{-10, -10}, {10,10}], {
"mousemove"->Function[xy,
target = xy;
EventFire["game", UpdatePositions, Null];
delay -= 1;
If[delay < 0,
globalProjectiles = Append[globalProjectiles, Projectile[xy + {0, 0.05}, {RandomReal[{-0.03,0.03}], 0.15}, 2]];
delay = 4;
];
]
}]};



Graphics[{
listener,
{Green, Rectangle[{1,-1}, {1.1, Offload[life] - 1}]},
{Blue, PointSize[0.1], Point @ Offload @ enemies},
RGBColor[1.0 - 0.1764, 1.0 - 0.8313, 1.0 - 0.74901], Point[Offload @ projectiles],
RGBColor[0.1764, 0.8313, 0.74901], PointSize[0.05], Point[Offload @ positions]
}, TransitionDuration->1, TransitionType->"Linear", PlotRange->{{-1,1}, {-1,1}}]
];

Players position is stored in target symbol, which is shared with a global variable players using global event "New player". On every change a global event with a topic (or pattern) UpdatePositions is fired, that forces all connected player to reload the positions of all connected players. The current client is also subscribed to the same global event.

For depicting enemies, projectiles and all players Dynamic symbols are used. However, there is a limitation, that for one symbol only one subscribed client is allowed. Therefore, for each client a local symbols such as enemies, projectiles and positions are allocated, to which all data is transferred once an update has been fired.

Limitations of graphical output

There is a limitation in this implementation, that all local symbols for moving objects are arrays and are plotted in a very primitive way as

{Blue, PointSize[0.1], Point @ Offload @ enemies},...

Since enemies length is a variable, it can extend and shrink its size based on number of alive enemies. This leads to a problem, that graphics library draw them as SVG objects. Then when a new enemy added or removed, it does not know, which one is which and just adds or removes the last one, while the all previous are considered to be the same with a new position read from a new array. It leads to serious problems if interpolation is used, i.e. when order of enemies is shifted in an array, all SVG objects jump and flicker. This is a reason, why TransitionDuration is set to 1, to get rid of interpolation mostly.

A better approach would be to threat them as individual graphics object on server as well. And dynamically add or remove them from the screen. So far this feature is still in development, but on can still use available methods of such as Call actions on a page to assign Javascript function to it.

Server's cycle

Here is is rather simple, there is a single continuous async task running in the background

calculate := (
time += 0.1;
calculateProjectile[];
calculateEnemies[];
spawnEnemies[];
)

for each calculation cycle it fires a global event

globalEnemies = {};
calculateEnemies[] := With[{},
globalEnemies = calculateEnemies /@ globalEnemies;
EventFire["game", UpdateEnemies, Null];
]