Forging History

projects python

www.forginghistorysaga.com

Forging History Saga: The Big Bang is a puzzle game for browser that I developed, collaborating with others, taking care of the software designing and programming and of the 3D computer graphics. I used mainly thre libraries: Three.JS, HTMLCanvas and GLSL as game engine and Angular 10 as single page website builder framework. As an online game there is also a back-end based on Python Django and served through Docker container on a server provided by Kamatera platform (migrated from PythonAnywhere in 2023) and a MySQL database on which user data are recorded.

Game area screenshot Game area screenshot

The game's goal is to recreate the conditions tha triggered the big bang and generated our universe. Main features:

  • collect the quanta to reach the appropriate balancing between temperature, pressure and energy to overcome various levels; unlock various typologies of quanta until anti-quanta gets out;
  • collect Quantum Energy and use it to unlock bonuses for improve scoring and overcome the most difficult levels;
  • play in Ranked mode, racking up scoring and climbing the players ranking;
  • use bonuses and boosters purchasable in the store; they can be bought exchanging them with the Closed Strings catched during the game in the levels or in the Ranked Mode;
  • unlock and collect the Periodic Table Elements achieving certain goals;
  • synchronize game data through multiple devices.

Inside the game platform therea are also advertising banners mediated by AdWords, integrated with the goal of covering server maintenance expenses and supporting the project.

Mobile version

A mobile version is available as well. Actually this was the original version on which the game idea was born, but in time we realized as, because of the typology of user interactions, it was much more pratical and versatile to handle a browser version and how the product was more adattable to this kind of platform. Mobile app origin source is the same as the browser one, but from a certain point ahead the two projects started to travel on different forks, for obvious reasons.

Angular source was compiled for Android through Capacitor 3 (migrated to Capacitor 4 in 2023) taking advance of the Apache Cordova libraries and the app was made available on Google Play Store.

From a technical point of view the making of the mobile app clashed several times against perfomarnce issues and even now most dated mobile devices are not able to guarantee a maximum-frame rate user experience; this is caused by the interaction between input touch and intersection recalculation, that requires an elevated amount of calculus to be executed at a very high frequency.

Three.JS v HTMLCanvas

To exploit at maxiumum the potentials offered by WebGL libraries, in this game making I largely used Three.JS. Someone may asks: Is it so necessary to implement a 3D render engine to make a bidimensional videogame?

The answer is 'absolutely yes'. First of all I must specify that Forgin History is not entirely bidimensional. There are game elements (as the Closed String and the bonuses) that are rendered on a tridimensional layer. Furthermore, having the Three.JS's Raycaster at disposal has made easier the making of interception detection dynamics between string, particles, Closed Strings and other game elements.

Then why don't use only Three.JS, instead of a hybrid system?

The choice of leveraging an hybrid system for the making of the game area was not taken on the technical analysis phase, but intstead during construction, following the emerging of some limitations both from a solution and the other. Because of that the game area is developed on several 2D/3D layers that overlap to obtain the desired result. The game area is, so, rendered through three different <canvas>:

  • WebGL canvas: it's the canvas on which the Three.JS renderer and the GLSL shaders work. Generated by WebGLRenderer, it receives and processes user inputs, executing all the logical calculations of the scene objects coordination; heree Closed Strings are rendered, as well as explosions and other tridimensional game elements;
  • gameCanvas: the canvas on which the fluctuating particles are drawn (so not during explosion) through context2D; the background is transparent to not cover WebGL canvas; on this widget pointers events are disabled to make user input go down to the underlying canvas;
  • stringCanvas: the canvas on which the string is visualized through context 2D; it has the same features of the gameCanvas. The only goal of this canvas is to render the luminous string.

Particles rendering

Although, paradoxically, the making of the particles in Three.JS allowed major performance than bidimensional rendering (currently in the mobile version only this typology is used) their the stylistic aspect was dull and less lively, and in order not to make it so it was necessary to implement certain scene lightsets which in any case would not have been proportional to obtaining a simple, but clean, cartoon effect. The use of bloom in the particle explosion phase, one of the graphic aspects that most characterizes the game, turned out to be excellent. Through the canvas, instead, particles were rendered simply by istancing, on the quanta coordinates, some sprites (PNG bidimensional pictures). I drew quanta's sprites on Gimp, giving them a liveler and "cartoon" style without having to edit lighting settings on the scene and without having to alter GLSL shaders, safeguarding explosions look. Canvas performance is lower than that of the WebGLRenderer, however it is such that it is irrelevant on modern PCs. The canvas layer, however, "steps aside" as the particles explode. In this phase the 3D particles emerge (always present, but remained invisible until that moment) to allow the appearance of the increasing luminescence effect. This effect is obtained, in the animation of the explosion, by increasing at each frame the RGB values of the GLSL custom shader which, interacting with the bloom of Three.JS, generates the golden "magic" of the explosion.

Study of space coordinates and intersections

To detect interceptions between user interaction and the scene elements, it was advantageous to use the Three.JS Raycaster. Indeed, game elements coordinates don't correspond with the canvas screen ones, but they must be converted again through unproject. OpenGL space coordinates, therefore, are the "real" coordinates of the game element, this because Three.JS has tourned out to be indispensable to make the aforementioned luminescent effect of the explosions. The method catchIntersection() of the CoreService (that I'll resume in a later paragraph) elaborates all the intersections. First, it uses the Raycaster to retrieve user interaction coordinates:

this.rayCaster.setFromCamera(this.utils.mouse, this.camera);
var unproj = this.rayCaster.ray.intersectPlane(this.planeMesh, new THREE.Vector3(0.0));

where this.utils.mouse contains the screen coordinates of the mouse itself and the method setFromCamera() returns the "virtual" ones. The same coordinates are also adapted to be used by the algorithm which detects the auto-intersection of the string, despite it lays on a bidimensional canvas layer. This is possible because it only matters that the comparison between the coordinates is valid in relative terms; the "real" points of the string are always recorded in this function and always as OpenGL space coordinates; the string's drawing-refresh, then, will convert them again into canvas space coordinates to render them on the screen in a proper way.

In short, on the data logical managing stage the string is managed in 3D space coordinates and is converted again in the 2D space only on the visualization stage.

let res = this.stringGeoService.checkSelfIntersection(unproj.x, unproj.z);
if (res === false) {
    this.stringGeoService.addPoint(unproj.x, unproj.z);
} else {
    this.utils.drawing = false;
    this.stringGeoService.clear();
}

If an intersection within the game area is detected then the stringGeoService proceeds to extend the string with a new point (addPoint(x, y)) always in 3D space coordinates; otherwise it triggers the string interruption and everything that goes with it.

Using some state machine global variables, the function unterstands if the user drawing of the string is occurring. If so, then begins the research of intersection between mouse and quanta, Closed String and other scene elements.

Taking for example the managing of the intersection with the qunta:

var allQuanta = this.cqs.getAllQuanta();
for (let i=0; i < allQuanta.length; i++) {
    if (allQuanta[i].x - DIST_TSHLD <= x && allQuanta[i].x + DIST_TSHLD >= x && allQuanta[i].z - DIST_TSHLD <= z && allQuanta[i].z + DIST_TSHLD >= z) {
        var result = this.quantumGeoService.handleQuantumSelection(allQuanta[i]);
        if (result === false){
            this.utils.drawing = false;
            this.stringGeoService.clear();
        }
    }
}

All the quantas's coordinates are compared with the mouse coordinates. If the difference between the two pairs is lower than the value of the DIST_TSHLD constant (a sensitivity threshold necessary to round the proximity) then an intersection will be detected. The quantumGeoService.handleQuantumSelection() method executes a series of elaborations concerning the selected quantum. For instance: if the player catches an anti-quantum or a quantum that is not compliant with those already caught with the same string, then it is expected that the string is going to expire. In this case it returns false and triggers the interruption of the string.

String rendering

As aforementioned, the string's point are sorted according to the WebGLRenderer space coordinates. Its rendering, however, happens inside the stringCanvas through HTMLCanvas procedures. As a new point is added to the string (and every second from dozens to hundreds of them are added, while the player is drawing it) the HTMLCanvas functions are invoked to draw a new line segment and, at the same time, the coordinate data are added to the array of point that make up the string, in addition to the verification of possible intersections. The coordinate's "virtual" data is then recorded by projecting mouse's coordinates on the 3D space coordinates, as seen before. For the actual string rendering, however, are directly used the screen coordinates of the mouse's movement resampling event.

this.ctx.lineTo(mouseX(event), mouseY(event));
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.moveTo(mouseX(event), mouseY(event));

The canvas is cleared when the string expires and then has to disappear. this.ctx.clearRect(0, 0, gameWidth(), gameHeight());

String intersection

By the game dynamics the user has to be able to catch the quanta (that are necessary to generate the groups to be exploded) hovering them with the mouse (or the finger in the mobile version) while a string has been activated. String's rendering was made using HTMLCanvas technology; during its making I often clashed against performance issues, issues for which I had to prefer some solutions than others. This string must deactivate when intercepts itself, as well as when the cursor stops moving; this is one of the main difficulty element of the game: the user is forced to mantain the string always moving, to not expire it, and, at the same time, has to follow paths that avoid the string to intersect itself (as happen in the famous game Snake).

To detect the intersection of what by the fact is a bidimensional curve (an assamble of points described by x,y coordinates) I needed to write down a function to be invoked each drawing refresh by the WebGL engine, that is at each frame rendered in real time. For this reason the algorithm had to be at the same time speed and reactive to trigger the interruption of the string as soon as the intersection occurred.

Base on a sensitivity factor defined by STRING_INTERSECTION_SENSIBILITY constant, the points that make up the string are processed, and their x, y coordinates (that I remind to be in OpenGL space coordinates and not in HTMLCanvas's) are compared to the last drawn point's. If the distance between one of the points of the string and the last drawn point of the string is lower than the tolerance threshold, the intersection is triggered. We must consider that the points are generated every millisecond when the mouse executes the drag to draw the string, and so also this function, checkSelfIntersection(), is invoked at the same frequency. The invocation is executed by the catchIntersection() function of the CoreService which handles the other intersections as well (for example the one between string and quanta) and in turn is invoked by the Javascript routine requestAnimationFrame().

Javascript animations

The making of various animations, that is of the autonomous movements of the game elements, was one of the more intresting components that I faced while making the project. One of the main problems was to choose which frame refresh operator use between setInterval() and requestAnimationFrame(). Due to performance issues on the heavier animations (ex. of the formation and explosions of the groups) the requestAnimationFrame() was preferred, despite some drawbacks. I used setInterval() together with setTimeout() only to configure some expiring timers, for example the expiring of bonuses. In fact, eventual lags on the order of milliseconds are tolerable on the timers that concern game infos, but they are damaging on timers that handle real time animations that have to be rendered on the screen. The requestAnimationFrame() guaranteed fluidity, syncrony and continuity in the elaboration of frames' sequences:

Frame skip is caused when the time between rendering frames is not in precise sync with the display hardware. Every so many frames a frame will be skipped producing inconsistent animation. [...] As most devices use 60 frames per second (or multiple of) resulting in a new frame every 16.666...ms and the timers setTimeout and setInterval use integers values they can never perfectly match the frame rate (rounding up to 17ms if you have interval = 1000/60). [...] requestAnimationFrame produces higher quality animation completely eliminating flicker and shear that can happen when using setTimeout or setInterval, and reduce or completely remove frame skips.

From Why is requestAnimationFrame better than setInterval or setTimeout? - Stack Overflow.

The group's creation animation was certainly the most sofisticated to make. The creaton of the group is invoked in the moment the string turns off. Quanta that has been catched by the user during the drawing of the string assume valorization of the catched attribute. The algorithm that performs group's animation selects all the quanta with catched=true and edits their position by interpolating their initial position with the final position of the group, that is a median of the positions of the selected quanta aimed to be uniformed within the group. Until now it looks like easy, but the diffculty of this animation lays on the needing of end the single quantum transition in the moment it "collides" with another one, in such a way that we have a group exposing all its members without overlapping. I must admit that the writing of the "collision" algorithms, due to a series of issues, was tougher than expected, but in the end I came by with a satisfying solution.

First of all, all the quanta are exempted from the animation of the group creation on the explosion stage; then the group speed is calculated related to the number of particles that make it up. In fact groups travel the slower the greather is the number of particles that make them up. At this point the targetPosition is defined, namely the initial coordinates of the group, a mean of the positions of the quanta that are going to form it. Then the single particles' target position is calculated; here the algorithm grows sophisticated because the final position must be set to not intercept the other particles' one. For this I wrote an algorithm to compare single particle coordinates with the others' one. A fragment of the complex algorithm that rules the generation of the groups follows:

private positionColliders(xz: any, xzSet: any[]): boolean {
    for (let i = 0; i < xzSet.length; i++) {
        var xzEl = xzSet[i];
        if (!xzEl){return false;}
        var distance = getDistance(xz.x, xz.z, xzEl.x, xzEl.z);
        if (distance < QUANTUM_RADIUS * GROUP_COMPRESSION) {
            return true;
        }
    }
    return false
}

After the generation of the final positions follows the configuration of an interval that will execute the animation by interpolating the current position with the target's one.

tp.q.x = lerp(tp.startingPos.x, tp.targetPos.x, 1);
tp.q.z = lerp(tp.startingPos.z, tp.targetPos.z, 1);

Periodic table of the elements

The Periodic Table is an user area where the gamer can view the elements that has been "won" during the game. In fact the user is able to "unlock" the 118 elements by reaching specific objectives. Inside the Periodic Table panel the user can verify which objectives are needed to unlock the elements as well as observe and "contemplate" the elements already won.

Screenshot of the Periodic Table of the Elements Screenshot of the Periodic Table of the Elements

There are further stages for the elements empowering: the anti elements and the bright elements. They are unlockable through further objectives, not implemented yet.

I made the 118 elements' rendering using Blender leveraging the proceduralism of the Principal shader's ramps to create maps of heightness, glowing, reflection and various metallic and rocky textures. Some examples are exposed on the above screenshot.

Security

The video game is freely distributed to be executed on compliant browsers, for this the source code of the front-end application is ispectionable from the browser console in use. The code has been obfuscated using the proper libraries the, furthermore, make its inspection and debugging complicated and avoid its reproduction on domains different from the original. This was done to avoid deobfuscations as much as possible, and so dramatically reducing eventual alterations of the algorythms or of the volatile memory data, alterations that could advantage the malicious user fraudulently.

The management of the webservice's security (via Web Token) and the SSO login have been handled likewise it was in A.Championship & Torunament, so I redirect to related paragraphs for a similar description of the integration process of those systems.

Verification of the authenticity of the match data

The user possesses its own authentication JWT (kept in cookies), so he could be able to invoke the game's REST service and add to himself the scoring related to a game match. Other types of operation, such as the bonus purchase in exchange of Closed Strings, are not possible because there is a further control layer on the server side.

To avoid this, at the beginning, I thought to strike up a polling system via websocket which had to allow to keep a living comunication session beween server and client from the start of the game match to its conclusion and consequent data recording. Unfortunately, however, the platform-as-service PythonAnywhere was not compatible with the websocket technology (Does pythonanywhere support websockets).

Consequently I studied a sophisticated HTTP calls protocollation system based on a series of unique encrypted codes that would identify the game match. This would allow that the saving of a game match is requested only by the client application and not by other sources (for example a tool like Postman or SoapUI). At the beginning of a new game match, the server generates a random game code encrypted via AES algorithm, leveraging Crypto.Cipher Python libraries. The encryption key is kept by the server (so is not accessible by anyone), but the front-end needs it too to decrypt the code and send it back, encrypted, to the server. In this way the encryption key could be discovered by inspectioning the front-end, a potential vulnerability. But the obastacles caused by the obsfucation libraries and a particular use of the assigning variables should make this operation effort not worth it.

At the end of the game match the client sends, via header HTTP, the encrypted code that the server is able to decrypt and compare to the one generated at the beginning of the game. If the code is wrong or there is no such header, the saving of the game match is invalidated.

In addition, I also implemented the existence of a secondary code. This one is generated by the server, at the end of the game match, following a front-end invokation, who sends secret information such for which the code generation won't be randomic, but that it will contain specific data that will be verified on the validation stage of the game match. Furthermore the server generates this secondary code by using another encrypting key, that this time is kept only on the server side and is not reachable from the client even if deobfuscated. I will not reveal here the way this code is generated to proctect its usefulness, it's enough to know that the validity of the game match is not verified by the server only by executing comparison of the decrypted unique randomic code, but also by anlayising the information held in the second code. Those informations are generated in such a way that even if an attacker were to discover the AES key, he would still not be able to regenerate a validatable code from the server.

Moerover, even the body of the HTTP request is encrypted by using another secret key. So anyone who wants to perform a fake HTTP call to the server to emulate the saving of a game match he never played would have to, in first instance, comprehend the JSON structure of the request body, then he would have to write it again with data that are congenial to him and, at the end, to encrypt all of it with a key he doesn't have, because it's hidden within the obfuscated code. It's useless to state that this last scruple strengthens even more the concealment of the content of the HTTP requests, that is already performed via HTTPS protocolling from the destination server.

The aforementioned end-point (single game match saving) is the only point of the REST service potentially exposed to unfair uses, this is why this particular management was orchestrated for it. The other end-points are made up by GET requests (read only) and so freely accessible to the user that holds its JWT, or by PUT requests that however excpect further controls in the back-end code on the data verification. For example an user might emulate the purchasing of a series of bonuses, even if he cannot afford it (he hasn't got the necessary Closed Strings to complete the purchase), nonetheless the server would not execute the transaction because it would verify the actual precence of that amount in the data kept on the server itself.

Updates

In 2023 I migrated the app target to the Android SDK 33 to be compatible with Android 12 and Android 13.

In the same year I dockerized the back-end app and served it on my server provided by Kamatera (same server on which this website is hosted) together with the related MySql database. Then, I also migrated the client app on the same server routing it through nginx proxy.

Browser changelog (last update on 2022/04/21).

Previous Post Next Post