This project is about making a simulation of an infectious virus propogation represented by a web game made with Ocsigen's eliom
This document assumes basic programming and ocaml knowledge
An eliom project is initialized using the ocsigen-start
CLI tool; which is a bootstrap software to get started with eliom projects.
Upon running the bootstrap command, I get the following folder structure ;
Since we are developing locally and dont have a public exposed hostname, we will need to change <host hostfilter="*">
to <host defaulthostname="localhost" hostfilter="*">
in our h42n42.conf.in
file. (This file specifies the configuration for oscigenserver, which is a seperate program what serves your files to the client when you run your code.); failture to do this will result in an error like so: ocsigenserver: main: Fatal - Error in configuration file: Incorrect hostname PC_NAME.
To run the example, one can do make test.byte
, and the default port should be 8080
.
Like the docs suggest, eliom is a full-stack web and mobile application framework with OCaml as its language. The special thing about eliom is that it follows a multi-tier architecture, which enables the user to generate server side and client side code with just one program.
This is acheiveable by using [%%client]
and [%%server]
directives. Though we will only be using client side code for this project.
multi-tier
Image credit: https://ocsigen.org/tuto/latest/manual/basics
We will be writing source code in .eliom files; the makefile rule will then generate server side code ran by ocamlopt and client side code run by a js engine.
More in-depth explanation can be found in the docs. Now that we have a rough idea on how our code compiles, we can write a simple client side widget that changes some state
A simple switch
Unlike other frameworks that handle the reactive binding for you, eliom and OCaml works closer to the native JS; which means for us to listen or react to some events, we need to keep the reacting component in a loop which continiously checks a state.
In the the example above, the function that renders the is_start?
block is in a loop which replaces itself with the actual value of the is_start
variable. This loop will run forever as long as the webpage is still open.
As for the buttons and user input, we need to develop our own transmuter between the JS DOM to our eliom app due to the strong typing in ocaml, we are unable to treat an element like a JS DOM object where we can change its elements and properties at our will. An example procedure to get input from a slider would be :
And at the rendering part, this function would be used like so :
As we mentioned above, we need to have a loop to constantly update states in the DOM tree. This would cause a problem where it would not be easy to keep track and sequence the updates as the application gets complex. LWT allows us to do these updates concurrently.
The diagram on the right is representing the use of concurrency
To use concurrency in eliom, it provides us with a LWT (LightWeight Thread) thread library which does async-pseudo concurrency under the hood.
For example, if we have a control panel section of the website and a game area of the website with a common state, we can spawn the threads respectively like so
And to return from a thread, one can use Lwt.return
. This will stop the thread from executing further and return the value which will be called by the async handler in the parent.
With the newfound knowledge, I have made 2 seperate sections of the game, one to control the game parameters and another is where the actual game takes place.
Every loop iteration of the game area, I would check if the game has started by the actions in the control panel. If its started, proceed by updating the game state by drawing the creets (Adding / updating the creet doms in the gamearea) and calculate its next position (top and left) then store it in the local state where we store the information for all the creets.
Which will result in a simple lava lamp like display
On top of that, the implementation of infections and mutations can also be implemented in the update_game function. The special mean mutation is implemented by changing the color and size of the element along with the position, and instead of a random direction, it selects another healthy creet and calculates the position based on that instead.
Mouse events for the creets can be implemented using the Js_of_ocaml_lwt.Lwt_js_events
module. Since there are 3 actions involving the mouse for the creets, mousedown
, mousemove
and mouseup
, we will have to register multiple handlers for the creet DOM elements.
They are sequential as well, we only need to register the mousemove
and mouseup
handlers once the mousedown
handler had been tripped, so that we dont have any handler code running when we are just hovering over the creet elements; it can be done like so
With the appropriate logic on top of the handlers, we can drag the creets now
This is also implemented with a timer which increments every iteration, to calculate the score and gameover condition
Since sick creets should be able to infect other creets upon contact, a way to detect of creets collide is needed. The follow is the algorithm to determine if 2 square creets are colliding :
This would need to be run for every other creet to know which creets are colliding with a single creet.
This approach would be inefficient since it still needs to compute the collision of creets which are far away, which are not colliding for sure; it would be more efficient if we only run this algorithm on creets which are closer instead.
We can do so using a quadtree; by seperating the game canvas into parts of 4, we are able to eliminate the 3 other parts which the creet does not belong in, since they are far from the creet we want to measure.
image credit: https://stackoverflow.com/questions/41946007/efficient-and-well-explained-implementation-of-a-quadtree-for-2d-collision-det
Say we want to know which particles are colliding with the blue particle, we can split the canvas into 4 quadrants, and we only run the detection algorithm on the particles on the bottom left quadrant since they have a closer proximity with the blue particle; All other particles are ignored
This is acheiveable by calculating the quadrants of each particle (by comparing midpoints of the bounds); This is possible due to our bounds are constant and we know the dimensions; to match the quadrant of the blue particle. If the said particle is not in the same quadrant, the collition detection algorithm wont be run.
We can also be more granular by splitting the canvas further into more fine quadrants, we just need to keep track of the quadrant path took by the blue particle to achive a more finer comparison.
The idea behind this approach instead of the clear-write-split approach is to prevent unessasary copies when bookeeping other states of the creet is done by another data structure.