G2k.js
G2k.js (GAME: 2 kilobytes) is an independent game development library developed in JavaScript as a challenge of space complexity. The entire project takes up only 2.01 kilobytes (2,058 bytes) of space.Overview
The programs used in our day-to-day lives are unbelievably bloated. Storage has become increasingly compact, affordable and accessible, causing many developers to lose sight of space efficient programs. While it is true that improved storage improves alleviates some need for small programs, large files can take an unbearable amount of time to load. Take, for example, Unreal Engine; a massively popular game engine that powers the likes of "Fortnite" and "S.T.A.L.K.E.R.". The most recent version of Unreal Engine uses over 30gb of disk space, and 200+mb for an empty project. Considering the nature of modern game development software, I challenged myself to see how small game engines can really become. I spent a few months in my spare time (June - September 2022) developing G2k.js to stretch the limit of how small a fully capable game engine can be. G2k.js has many expected features of a game engine. These features as follows: Game window initialization, Sprites (Initialization, movement, animation, rotation), Rendering (Automated, with options to force a render), collisions (sprite over sprite & mouse over sprite), timed loops, drawing (circles, rectangles, text), random number generation (RNG with min/max), mouse position global variables, data saving and loading (Using cookies), Audio (Musical note synths, play audio files with control options), and many events that can be used to trigger code. The final version of G2k is as follows:
Documentation
Game Initialization
Game initialization can be called using the init function: G.init(gameWidth, gameHeight, cssClass) All parameters are required, and they will allow you to set the width of the canvas in which the game runs, as well as the game's height and CSS Class for any styling.
Sprites
Sprite initialization can be called using the sprite init function: G.sprite.init(id, imageSource, xPosition, yPosition) The id (numeric) will be used to reference the sprite. Though the other parameters are required, they can be placeholder as they're all able to be changed. Sprite movement can be called using the sprite move function: G.sprite.move(id, x, y, relative) The x and y refer to the new x and y position of the sprite. The "relative" parameter (optional) can be set to true, which will change the position relative to the sprite's current position. Another option for sprite movement is the moveDir function: moveDir(id, distance) The moveDir funciton will move the sprite by the inputted distance in the direction which it is facing. Sprite rotation can be called using the sprite's rotate or rotTo functions: G.sprite.rotate(id, degrees, relative) This will set the rotation in degrees, or rotate it from it's current rotation if relative is set to true. The second function, rotTo can be used as follows: G.sprite.rotTo(id, x, y) The x and y parameters are the x and y position to which you'd like to rotate the sprite. If you'd like to rotate it to another sprite, this can be achieved like so: rotTo(id, G.sprites[otherId][1], G.sprites[otherId][2]) Sprite animation and image source changes can be achieved using the sprite change function: G.sprite.change(id, newSource) The newSource must be an accessible URL, and can be periodically changed in order to create an animated effect.
Rendering
Rendering uses the render function as such: G.render(delay, step) Both values are optional. If no delay is defined, the game window will immediately re-render. If a delay is defined, the game will re-render every (time) ms. For example, if you'd like the engine to run at 30 FPS, you can call G.render(33) The step parameter is an optional function that will execute on each game render cycle, before the rendering actually occurs.
Events
Key events can be achieved using the key event function: G.event.key(eventType, key, function) eventType can be "press", "down" or "up". Key is the key (in quotes) to detect ex: " " or "w". The function is the function to execute upon the keypress. Click events can be achieved using the click event function: G.event.click(spriteId, function) spriteId is the sprite to which you'd like to detect a cursor click, and the second parameter is the function to execute upon being clicked.
Collisions
Collisions can be used to detect whether a border or sprite is colliding with another sprite: G.isCollide(idOne, idTwo|border) The function will return true or false if the two inputted sprites are colliding. If the second parameter is "border" or "b", it will return true/false depending on whether the inputted sprite is colliding with the border. Collision of a point can be checked using the cp (Check point) function: G.cp(pointArray, spriteId) The point array must be an array of length two containing an x and y point such as [100, 250]. The spriteId is the sprite for which you'd like to check this point.
Timed Loops
Timed loops can be achieved using the loop function: G.loop(function, delay, condition) This will execute the inputted function every <delay> ms, as long as the condition is true.
Drawing
Rectangles can be drawn with the sprite rect function: G.draw.rect(x, y, width, height, color, fill) The fill is an optional (default false) boolean value that will indicate whether to fill the rectangle. Color must be a string (can be rgb, hex, color code, etc.). The remaining values must be int or double. Circles can be drawn with the sprite circle function: G.draw.circle(x, y, radius, color, fill) The fill is an optional (default false) boolean value that will indicate whether to fill the rectangle. Color must be a string (can be rgb, hex, color code, etc.). The remaining values must be int or double. Text can be drawn with the sprite text function: G.draw.text(text, x, y, fontSize, font, color) All values are required. The fontSize can be any html accepted value eg: "14px", and the same applies for the font eg: "serif".
Audio
Musical notes can be played with the built-int note synthesizer feature: G.audio.note(note, type, length, delay) The note is a string written as note, sharp, octave eg: "G#4" or "A6". Type is the shape of the oscillator wave; options are "sine", "square", "sawtooth", and "triangle". You can read the MDN docs for more information. The length and delay are each in seconds and define the length of the sound and delay until it is played. Audio files can be played using the multiple audio control functions, beginning with add: G.audio.add(audioId, filePath) The audioId should be a numeric id that indicates the audio file, it is recommended to begin with 0 and increase the id with each added file. The file path is an accessible path to the audio file that should be played. The second audio control function is play: G.audio.play(audioId, repeat) The audioId is the same as you set in the audio.add function. The repeat is a true/false value (Default false), that will optionally loop the audio file upon finishing if set to true. The final audio control function is stop: G.audio.stop(audioId) This will immediately end the audio from playing, even if you have previously set it to repeat.
Randomization
Randomization can be achieved with random(min, max, int) The funciton will return an integer or double, as specified in the function. The "int" parameter defines whether the function should return an integer (true) or double (false).
Data Storage
Data storage in G2k is very straight-forward and is controlled by two functions. The first is set: set(key, data) This will set the data retrievable by its key to the data parameter. The second function is get: get(key) the function will return the data stored at the provided key value.
Source Code
_='G={s:[d:[a:zConxt,Z:0,init=creaElemJt("CANVAS".classList.add(r =t,=e,tx=.getConxt("2d"body.appJdChild(.onmouQmove=t=>(xX,yY:{init$,d,e,rUa=new Image,i=[];r6(i=[d,,rU,0]a.src=e,i=a,t`i%moveUs?(2]+=e,3]+=r):(2`e,3`rrotas=PI/180*e;r?4]+=s:4`s%clonei=s.lJgth;i`t].sliceW;r6(i][2`e,i][3`r); i%Z0`e%change1].src=e%moveDirr=4];2]+=sin(r)*e3]-=cos(r)*erotTos=t];4`-atan2(e-s[2r-s[3])}%rJder@,r@eW;9e,r=Ujs.clearRect(0,0,r ,r);s.forEach(a=>{Z==a[0]6(o=a /2,n=a/2U.save(Ka[2]+o,a[3]+ns.rota(a[4]K-o,-ns.drawImage(a[10,0s.restoreW)});r(t6^frJder$tevJt:{key$,kU@ key"+t,e=>e.key==k6nWclick$,r@ click",e=>p([x,yt)6Z==0]6rW)}%isCollides=ta=s ,i=s,o=s[2n=s[3];if(isNaN(e))for(9gt=0;t<4;t++g=rp(g[0gs[4]);9m=g[0k=g[1y=;if(m<0||m>y ||k<0||k>y)!0}elQ for(9c=ed=c ,l=c,h=c[2p=c[3vu=[[h,p
[h,p+l
+l]f=0;f<4;f++)if(p(v[fe)||p(u[ft))!0%loopr6(Ft)(^W=>loope)draw:{reca,oa,o.rectUi?oW:o(circleU,aijis,is,i.arc,0,2*PIi(a6i(xi,o.font=s+" "+a,oText)}%audio:{noU=0,v=a.currJtTimewith(a)with(creaOscillatorW)start(v+s,connect(destinationfrequJcy.value=2**(+$[2]??t)+("C0D0EF0G0A0B".Qarch$[0])+/#/.st$)-57)/12)*440,type=e)+stop(v+r+saddd=z(eplayr=d;e6r. Jded",f!1stop$d.pauQW}%QtHQtImget:t=>HgetIm$randoms=randomW*(e-t)+t; r?~~s:s%rpU,ai=cos(ao=sin(an=i*$-r)-or,c=io*$-r)+s;[nc)]%cpr=es=r[1a=s ,i=s,o=r[2n=r[3c=rp$[0t-r[4]d=c[0l=c; d>o6d<o+a6l>n6l<n+i}}s[$,e9Math.t][.height],G. addEvJtLisner(",r .width=[[o,n[o,n+i+i])%return),.fillspriStyle=.strokemouQ_round(c[1]){tU,a,iojote[t]unction([1o+a/2,n+i/2,$(t%},6&&9var @=n=>nHlocalStorage.JenKs.transla(QseU,sW()Zroom^QtTimeout(`]=j=tx;znew Audiovoid 0!==documJt.=t.offQtr.play(*(e-s)+[o+a,n
[h+d,p';for(Y in $='
zj`^ZWUQKJH@96%$ ')with(_.split($[Y]))_=join(pop());eval(_)
You might ask yourself: "What the hell is going on here?", and that'd be justified. This code is crushed (Curtosey of aivopaas's JSCrush). JSCrush is a JavaScript packer that minimizes code through string replacements. This code will likely display many unknown characters due to limitations of the UTF-8 charset, however, the code can still be copy-pasted and execute correctly. To make some more sense of what is going on, here's the less obfuscated, uncrushed version of G2k:
const G={sprites:[],d:[],a:new AudioContext,room:0,init(t,e,r){G.c=document.createElement("CANVAS"),G.c.classList.add(r),G.c.width=t,G.c.height=e,G.ctx=G.c.getContext("2d"),document.body.appendChild(G.c),G.c.onmousemove=t=>(G.mouse_x=t.offsetX,G.mouse_y=t.offsetY)},sprite:{init(t,d,e,r,s){var a=new Image,i=[];void 0!==r&&(i=[d,,r,s,0]),a.src=e,i[1]=a,G.sprites[t]=i},move(t,e,r,s){s?(G.sprites[t][2]+=e,G.sprites[t][3]+=r):(G.sprites[t][2]=e,G.sprites[t][3]=r)},rotate(t,e,r){var s=Math.PI/180*e;r?G.sprites[t][4]+=s:G.sprites[t][4]=s},clone(t,e,r){var i=G.sprites.length;G.sprites[i]=G.sprites[t].slice();r&&(G.sprites[i][2]=e,G.sprites[i][3]=r);return i},room(t,e){G.sprites[t][0]=e},change(t,e){G.sprites[t][1].src=e},moveDir(t,e){var r=G.sprites[t][4];G.sprites[t][2]+=Math.round(Math.sin(r)*e),G.sprites[t][3]-=Math.round(Math.cos(r)*e)},rotTo(t,e,r){var s=G.sprites[t];G.sprites[t][4]=-Math.atan2(e-s[2],r-s[3])}},render(t,e=n=>n,r=n=>n){e();var e,r=G.c,s=G.ctx;s.clearRect(0,0,r.width,r.height);G.sprites.forEach(a=>{G.room==a[0]&&(o=a[1].width/2,n=a[1].height/2,s.save(),s.translate(a[2]+o,a[3]+n),s.rotate(a[4]),s.translate(-o,-n),s.drawImage(a[1],0,0),s.restore())});r(),void 0!==t&&setTimeout(function(){G.render(t)},t)},event:{key(t,k,s=n=>n){addEventListener("key"+t,e=>e.key==k&&n())},click(t,r=n=>n){addEventListener("click",e=>G.cp([G.mouse_x,G.mouse_y],t)&&G.room==G.sprites[t][0]&&r())}},isCollide(t,e){var s=G.sprites[t],a=s[1].width,i=s[1].height,o=s[2],n=s[3];if(isNaN(e))for(var g=[[o,n],[o+a,n],[o,n+i],[o+a,n+i]],t=0;t<4;t++){g[t]=G.rp(g[t][0],g[t][1],o+a/2,n+i/2,s[4]);var m=g[t][0],k=g[t][1],y=G.c;if(m<0||m>y.width||k<0||k>y.height)return!0}else for(var c=G.sprites[e],d=c[1].width,l=c[1].height,h=c[2],p=c[3],v=[[o,n],[o+a,n],[o,n+i],[o+a,n+i]],u=[[h,p],[h+d,p],[h,p+l],[h+d,p+l]],f=0;f<4;f++)if(G.cp(v[f],e)||G.cp(u[f],t))return!0},loop(t,e,r){r&&(Function(t)(),setTimeout(()=>G.loop(t,e,r),e))},draw:{rect(t,e,r,s,a,i){var o=G.ctx;o.strokeStyle=a,o.fillStyle=a,o.rect(t,e,r,s),i?o.fill():o.stroke()},circle(t,e,r,s,a){var i=G.ctx;i.strokeStyle=s,i.fillStyle=s,i.arc(t,e,r,0,2*Math.PI),i.stroke(),a&&i.fill()},text(t,e,r,s,a,i){var o=G.ctx;o.fillStyle=i,o.font=s+" "+a,o.fillText(t,e,r)}},audio:{note(t,e,r,s=0,v=G.a.currentTime){with(G.a)with(createOscillator())start(v+s,connect(destination),frequency.value=2**(+(t[2]??t[1])+("C0D0EF0G0A0B".search(t[0])+/#/.test(t)-57)/12)*440,type=e)+stop(v+r+s)},add(t,e){G.d[t]=new Audio(e)},play(t,e){var r=G.d[t];r.play(),e&&r.addEventListener("ended",function(){r.play()},!1)},stop(t){G.d[t].pause()}},set(t,e){localStorage.setItem(t,e)},get:t=>localStorage.getItem(t),random(t,e,r){var s=Math.random()*(e-t)+t;return r?~~s:s},rp(t,e,r,s,a){var i=Math.cos(a),o=Math.sin(a),n=i*(t-r)-o*(e-s)+r,c=i*(e-s)+o*(t-r)+s;return[Math.round(n),Math.round(c)]},cp(t,e){var r=G.sprites[e],s=r[1],a=s.width,i=s.height,o=r[2],n=r[3],c=G.rp(t[0],t[1],o+a/2,n+i/2,-r[4]),d=c[0],l=c[1];return d>o&&d<o+a&&l>n&&l<n+i}}