shaku
Version:
A simple and effective JavaScript game development framework that knows its place!
712 lines (603 loc) • 23.4 kB
HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Shaku</title>
<meta name="description" content="Shaku - a simple and easy-to-use javascript library for videogame programming.">
<meta name="author" content="Ronen Ness">
<link href="css/style.css" rel="stylesheet" type="text/css" media="all">
</head>
<body>
<div style="padding:0.5em" class="noselect">
<div id="top-info">
<h1 class="demo-title" style="margin-left: 0;">Shaku: Fishy</h1>
<p>Based on the Flash game classic <a target="_blank" href="https://newgrounds.fandom.com/wiki/!_Fishy_!">!Fishy!</a>: Eat smaller fish to grow, dodge bigger fish to survive.<br />
Use <b>Arrows</b> or <b>WASD</b> to move. Hit <b>R</b> to restart.</p>
<!-- include shaku -->
<script src="js/demos.js"></script>
<script src="js/shaku.js"></script>
<button style="font-size: 200%;" onclick="runGame(); this.style.display='none'; document.getElementById('loading-msg').style.display='block';">Click To Start!</button>
<h1 id="loading-msg" style="display: none;">Loading Assets...</h1>
</div>
<!-- demo code -->
<script id="main-code">async function runGame()
{
// init shaku
await Shaku.init();
// add shaku's canvas to document
document.body.appendChild(Shaku.gfx.canvas);
// create bottom text
let bottomText = document.createElement('h3');
document.body.appendChild(bottomText);
// hide top info
document.getElementById('top-info').style.display = 'none';
// set canvas size
let screenX = 1100;
let screenY = 800;
Shaku.gfx.setResolution(screenX, screenY, true);
// load all assets
let assets = {};
async function loadAssets() {
// load font texture
Shaku.assets.loadFontTexture('assets/DejaVuSansMono.ttf', {fontName: 'DejaVuSansMono'}).then((asset) => assets.fontTexture = asset);
// load textures into a texture atlas
let textureSources = [
'assets/fishy/skeleton_icon.png',
'assets/fishy/fish_skeleton.png',
'assets/fishy/fish.png',
'assets/fishy/eye.png',
'assets/fishy/bubble.png',
'assets/fishy/bones.png',
];
Shaku.assets.createTextureAtlas('texture_atlas', textureSources, undefined, undefined, new Shaku.utils.Vector2(12, 12)).then((asset) => {
let i = 0;
assets.textures = asset;
assets.skeletonIcon = asset.getTexture(textureSources[i++]);
assets.fishSkeleton = asset.getTexture(textureSources[i++]);
assets.fish = asset.getTexture(textureSources[i++]);
assets.eye = asset.getTexture(textureSources[i++]);
assets.bubble = asset.getTexture(textureSources[i++]);
assets.bones = asset.getTexture(textureSources[i++]);
});
// load background texture outside the atlas - due to wave effect
assets.background = await Shaku.assets.loadTexture('assets/fishy/background.png');
// load seaweed texture outside the atlas - due to wave effect
assets.seaWeed = await Shaku.assets.loadTexture('assets/fishy/seaweed.png');
// load sounds
assets.sfx = {};
Shaku.assets.loadSound('assets/fishy/fishing.ogg').then((asset) => assets.sfx.music = asset);
Shaku.assets.loadSound('assets/fishy/bubble.ogg').then((asset) => assets.sfx.bubble = asset);
Shaku.assets.loadSound('assets/fishy/burp.ogg').then((asset) => assets.sfx.burp = asset);
// wait for all assets to load
await Shaku.assets.waitForAll();
};
await loadAssets();
document.getElementById('loading-msg').style.display = 'none';
// set everything to smooth scaling
Shaku.gfx.defaultTextureFilter = Shaku.gfx.TextureFilterModes.Linear;
// create a wavy water effect
class WavyWaterEffect extends Shaku.gfx.SpritesEffect
{
get fragmentCode()
{
const fragmentShader = `
#ifdef GL_ES
precision highp float;
#endif
uniform sampler2D texture;
uniform float elapsedTime;
uniform float waveFactor;
varying vec2 v_texCoord;
varying vec4 v_color;
void main(void) {
float offset = (cos(v_texCoord.y * 10.0 + elapsedTime) / 100.0) * waveFactor;
gl_FragColor = texture2D(texture, v_texCoord + vec2(offset, 0.0)) * v_color;
gl_FragColor.rgb *= gl_FragColor.a;
}
`;
return fragmentShader;
}
get uniformTypes()
{
let ret = super.uniformTypes;
ret['elapsedTime'] = { type: Shaku.gfx.Effect.UniformTypes.Float };
ret['waveFactor'] = { type: Shaku.gfx.Effect.UniformTypes.Float };
return ret;
}
}
// create sprites and text batch
let spritesBatch = new Shaku.gfx.SpriteBatch();
let textSpriteBatch = new Shaku.gfx.TextSpriteBatch();
// set outline
textSpriteBatch.outlineWeight = 1;
textSpriteBatch.outlineColor = Shaku.utils.Color.black;
// create effect for moving water
let waterEffect = new WavyWaterEffect();
// randomize a fish color
let fishColors = [];
for (let r = 0; r < 4; r++) {
for (let g = 0; g < 4; g++) {
for (let b = 0; b < 4; b++) {
if (r + g + b >= 3) {
fishColors.push(new Shaku.utils.Color(r / 3, g / 3, b / 3));
}
}
}
}
function randomizeFishColor() {
return fishColors[Math.floor(Math.random() * fishColors.length)];
}
// define a fish instance
class Fish
{
constructor()
{
this._group = new Shaku.gfx.SpritesGroup();
let fishSource = assets.fish.sourceRectangle;
this._body = new Shaku.gfx.Sprite(assets.fish, new Shaku.utils.Rectangle(0, 0, fishSource.width, fishSource.height / 2));
this._body.size.set(fishSource.width, fishSource.height / 2);
this._body.color = randomizeFishColor();
this._group.add(this._body);
this._eye = new Shaku.gfx.Sprite(assets.eye);
this._eye.size.copy(assets.eye.sourceRectangle.getSize());
this._eye.position.set(265, -40);
this._group.add(this._eye);
this._animFactor = Math.random() * 5;
this._timeToSpawnBubble = Math.random();
this.setSize(1);
this.position.set(screenX / 2, screenY / 2);
}
doBeatAnimation()
{
(new Shaku.utils.Animator(this._group)).to({'scale': this._group.scale.mul(1.165)}).repeats(1, true).duration(0.215).smoothDamp(true).play();
}
get isFlipped()
{
return this._group.scale.x < 0;
}
get movingAnimationSpeed()
{
return 1;
}
get position()
{
return this._group.position;
}
set position(val)
{
this._group.position = val;
}
setSize(size)
{
let factoredSize = Math.floor((size / 5) * 100) / 100;
this._group.scale.set(factoredSize * (this.isFlipped ? -1 : 1), factoredSize);
this._size = size;
}
getSize()
{
return this._size;
}
getCollisionRect()
{
let width = Math.abs(this._group.scale.x * assets.fish.sourceRectangle.width * 0.655);
let height = this._group.scale.y * assets.fish.sourceRectangle.height * 0.5 * 0.645;
return new Shaku.utils.Rectangle(this.position.x - width / 2, this.position.y - height / 2, width, height);
}
updateAndDraw()
{
// draw and animate
spritesBatch.drawSpriteGroup(this._group);
this._animFactor += 0.15 * this.movingAnimationSpeed;
this._body.sourceRectangle.y = (Math.floor(this._animFactor) % 2) * 512;
this._eye.rotation = Math.sin(Shaku.gameTime.elapsed * 50) / 15;
// spawn bubbles
if (this._size > 0.2) {
this._timeToSpawnBubble -= Shaku.gameTime.delta;
if (this._timeToSpawnBubble < 0) {
this._timeToSpawnBubble = 2 + Math.random() * 4;
let position = this.position.add({x: this._group.scale.x * assets.fish.width * 0.35, y: 0});
game.addBubble(this._size * 30, position);
}
}
}
get shouldBeRemoved()
{
return false;
}
}
// fish for enemy fish
class NpcFish extends Fish
{
constructor(size)
{
super();
this.setSize(size);
this.position.y = Math.random() * screenY;
let speed = 27.5 + (Math.random() * 120);
if (Math.random() < 0.5) {
this.position.x = -this._group.scale.x * 1024;
this._speed = speed;
}
else {
this.position.x = screenX + this._group.scale.x * 1024;
this._group.scale.x *= -1;
this._speed = -speed;
}
}
get shouldBeRemoved()
{
return (this._speed > 0 && this.position.x > screenX + this._group.scale.x * this._body.size.x) ||
(this._speed < 0 && this.position.x < this._group.scale.x * this._body.size.x);
}
updateAndDraw()
{
super.updateAndDraw();
this.position.x += this._speed * Shaku.gameTime.delta;
}
}
// fish for player fish
class PlayerFish extends Fish
{
constructor(size)
{
super();
this.setSize(size);
this.position = new Shaku.utils.Vector2(screenX / 2, 0);
this.velocity = new Shaku.utils.Vector2(0, 2.5);
this._body.color = Shaku.utils.Color.orange;
this._moveAcc = 5;
}
get movingAnimationSpeed()
{
return Math.min(Math.abs(this.velocity.length() / 17.5), 1.25) * 10;
}
get shouldBeRemoved()
{
return false;
}
updateAndDraw()
{
// call base update
super.updateAndDraw();
// move fish
this.position.x += this.velocity.x * Shaku.gameTime.delta * 100;
this.position.y += this.velocity.y * Shaku.gameTime.delta * 100;
// keep in screen
if (this.position.x < 0) {
this.position.x = 0;
if (this.velocity.x < 0) this.velocity.x = 0;
}
if (this.position.y < 0) {
this.position.y = 0;
if (this.velocity.y < 0) this.velocity.y = 0;
}
if (this.position.x > screenX) {
this.position.x = screenX;
if (this.velocity.x > 0) this.velocity.x = 0;
}
if (this.position.y > screenY) {
this.position.y = screenY;
if (this.velocity.y > 0) this.velocity.y = 0;
}
// do movement
let flipLeft = false;
let flipRight = false;
if (Shaku.input.down('left') || Shaku.input.down('a')) { this.velocity.x -= this._moveAcc * Shaku.gameTime.delta; flipLeft = true; }
else if (Shaku.input.down('right') || Shaku.input.down('d')) { this.velocity.x += this._moveAcc * Shaku.gameTime.delta; flipRight = true; }
if (Shaku.input.down('up') || Shaku.input.down('w')) { this.velocity.y -= this._moveAcc * Shaku.gameTime.delta; }
else if (Shaku.input.down('down') || Shaku.input.down('s')) { this.velocity.y += this._moveAcc * Shaku.gameTime.delta; }
// flip sprites
if (flipLeft) {
this._group.scale.x = -Math.abs(this._group.scale.x);
}
else if (flipRight) {
this._group.scale.x = Math.abs(this._group.scale.x);
}
// damp velocity
this.velocity = Shaku.utils.Vector2.lerp(this.velocity, Shaku.utils.Vector2.zero(), Shaku.gameTime.delta * 0.75);
}
}
// an animated bubble
class Bubble
{
constructor(size, position)
{
this.position = position;
this.size = size;
this.animFactor = Math.random() * 3;
}
updateAndDraw()
{
spritesBatch.drawQuad(assets.bubble.texture,
this.position.add({x: Math.cos(this.animFactor + Shaku.gameTime.elapsed * 5) * this.size * 0.2, y: 0}),
this.size,
assets.bubble.sourceRectangle);
this.position.y -= this.size * 1.25 * Shaku.gameTime.delta;
}
get shouldBeRemoved()
{
return this.position.y < -this.size;
}
}
// an animated bones
class Bones
{
constructor(size, position, flipX)
{
this.position = position;
this.size = size * 120;
this.flipX = flipX;
this.animFactor = Math.random() * 3;
}
updateAndDraw()
{
spritesBatch.drawQuad(assets.fishSkeleton.texture,
this.position.add({x: Math.cos(this.animFactor + Shaku.gameTime.elapsed * 5) * this.size * 0.2, y: 0}),
new Shaku.utils.Vector2(this.size * (this.flipX ? -2 : 2), this.size), assets.fishSkeleton.sourceRectangle);
this.position.y += this.size * 1.25 * Shaku.gameTime.delta;
}
get shouldBeRemoved()
{
return this.position.y > screenY + this.size;
}
}
// current game instance
class Game
{
constructor()
{
// create new game state
this.fish = [];
this.bubblesAndBones = [];
this.score = 0;
this.player = new PlayerFish(this.getPlayerSize());
this._timeToGenerateFish = 2.5 + Math.random() * 1;
// start music and start effect
Shaku.sfx.stopAll();
Shaku.sfx.play(assets.sfx.bubble);
this._music = Shaku.sfx.createSound(assets.sfx.music);
this._music.loop = true;
this._music.volume = 0.215;
this._music.preservesPitch = true;
this._music.play();
this._onScoreUpdate();
// is it game over?
this._gameOver = false;
}
stop()
{
this._music.stop();
}
getPlayerSize()
{
return (35 + this.score) / 100;
}
addBubble(size, position)
{
let bubble = new Bubble(size, position);
this.bubblesAndBones.push(bubble);
}
addBones(size, position, flipX)
{
let bones = new Bones(size, position, flipX);
this.bubblesAndBones.push(bones);
}
gameOver()
{
this.addBones(this.player.getSize(), this.player.position, this.player.isFlipped);
this._music.stop();
this._gameOver = true;
}
get messedUpFactor()
{
return Math.max(Math.min((this.score - 25) / 500, 1), 0);
}
playerEats(enemy)
{
this.score += Math.max(enemy.getSize() * 5, 1);
this.addBones(enemy.getSize(), enemy.position, enemy.isFlipped);
this._onScoreUpdate();
}
_onScoreUpdate()
{
// set player size and do pulse animation
this.player.setSize(this.getPlayerSize());
this.player.doBeatAnimation();
// distort music
this._music.playbackRate = 1 - this.messedUpFactor * 0.75;
// stop music
if (this.score >= 1000) {
(new Shaku.utils.Animator(this._music)).to({volume: 0}).duration(10).play();
}
// end of game!
if (this.score >= 5000) {
bottomText.innerHTML = "I̷͉̩̤̅̌̐ ̵̫̯͑f̴̨͋o̸͔̱͂ū̷̧̫̹̐̿n̵̺͔̔̑̔d̴̻͛ ̴̝̹͂̾̓ä̸̙͈́ ̷͇̱̫͋w̴̝̏̇ä̴̲́̄ͅy̶͎̌ ̶̤͕͊̓o̶̱͑u̵͇̝͂̐̕ţ̵̹͗.̵̪͈͚̇̕ ̵̼̮͚͋̓͛Ị̷̮̗̏ ̵̪́̎a̴̡̩̖̅m̴̘̘̀͠ ̵͓̣̌f̵͍̣̎̔r̴̛͍͙̟ẻ̴̞͕e̶͕̖̒̑̂͜.̸̮͂";
this._gameOver = true;
}
else if (this.score >= 4000) {
bottomText.innerHTML = "Š̷̫͐ǒ̴̤̕o̷̡̢͉͛̈́n̴̟̰͑͆̾.̸̯̍.̷͕̝̍͋.̷̭͝.̵͔̯̏̓̍";
}
else if (this.score >= 2000) {
bottomText.innerHTML = "Ṛ̷̢̾ȩ̷̬͘l̴͓̇͆ȩ̷͘à̵̺ŝ̷̝̙e̷̛̛̲ ̷̛̙͝ṃ̴͖̈́̓ḗ̵̦ ̴̟̗̊f̸̯̦́̆r̸̪̺̔̕o̵̭̽m̸̻̫͝ ̷̻̳́̎m̷͔̞͛y̸̯̋ ̸͕̔p̵̙̦̒r̶͓̐ĩ̶̺̦s̴̠̦͆ö̸͎́n̶̏̂͜.̷̰̀͋.̷̪̘̐.̷̹͝.̸̟͑͝";
}
else if (this.score >= 1000) {
bottomText.innerHTML = "M̴̪͛͑ý̴̭́ ̴̲͆̐T̶̗̓͑h̸̲̯̏̎r̵͍̲̅ơ̶̱̍n̸͐̅͜ë̶̮͚̾ ̶̜͊Ő̴̹͝f̶͍͍̒̚ ̶̠̹̋̊F̴̝̕l̸͑͜͝e̷̱̚ͅs̸̨̆͝h̴̗̽͘ ̶̀ͅA̶̢̎͘ẉ̴́͊a̴̟̒̇ḯ̵̡͛ͅt̸̯̿̈s̶̪̙̋.̷̖̂́";
}
else if (this.score >= 800) {
bottomText.innerHTML = "Í̴̻̺ ̷͚̎͜h̴̳͎͆̕ȕ̸͖n̸̮̽͑g̵̯̑e̶͕̠̊̀ṟ̴̐.̶͕̗̕";
}
else if (this.score >= 700) {
bottomText.innerHTML = "M̷̨̕ọ̸̃r̵̦̅é̴͇!̸̘̿ ̵̢̈́M̵̪̓o̵͙͂ŕ̶̗ę̸̆!̸̰̋ ̵̙͝M̴̊ͅö̶͜r̶̦͝e̴̔ͅ!̶͍͋";
}
else if (this.score >= 600) {
bottomText.innerHTML = "I̸ ̵c̶a̵n̴'̵t̷.̶.̶ ̸S̷t̷o̴p̷.̵.̴";
}
else if (this.score >= 500) {
bottomText.innerHTML = "Y̵e̶s̷s̸s̵s̵s̷.̷.̴.̴.̵.̴ ̴";
}
else if (this.score >= 400) {
bottomText.innerHTML = "S̵o̸ ̷m̶u̵c̵h̶ ̷p̵o̶w̸e̴r̸.̵.̴";
}
else if (this.score >= 300) {
bottomText.innerHTML = "I'm Unstoppable.";
}
else if (this.score >= 250) {
bottomText.innerHTML = "I shall inherit the seas. Nay. The WORLD.";
}
else if (this.score >= 200) {
bottomText.innerHTML = "I Grow Stronger.";
}
else if (this.score >= 180) {
bottomText.innerHTML = "I'm starting to like this.";
}
else if (this.score >= 140) {
bottomText.innerHTML = "Maybe I should stop..";
}
else if (this.score >= 100) {
bottomText.innerHTML = "I'm not even hungry anymore.";
}
else if (this.score >= 70) {
bottomText.innerHTML = "I.. I think that's enough..";
}
else if (this.score >= 50) {
bottomText.innerHTML = "That was delicious!";
}
else if (this.score >= 25) {
bottomText.innerHTML = "Hmmm... Yum!";
}
else {
bottomText.innerHTML = "I'm a hungry hungry fishy!";
}
}
updateAndDraw()
{
// draw background with water effect
spritesBatch.begin(undefined, waterEffect);
waterEffect.uniforms.elapsedTime(Shaku.gameTime.elapsed);
waterEffect.uniforms.waveFactor(1);
spritesBatch.drawRectangle(assets.background, Shaku.gfx.getRenderingRegion());
spritesBatch.end();
spritesBatch.begin();
// draw bones pile
spritesBatch.drawQuad(
assets.bones,
new Shaku.utils.Vector2(screenX / 2, screenY),
assets.bones.sourceRectangle.getSize(),
undefined, null, 0, new Shaku.utils.Vector2(0.5, this.messedUpFactor * 0.95));
// update and draw fish
for (let i = this.fish.length - 1; i >= 0; --i) {
let fish = this.fish[i];
fish.updateAndDraw();
if (fish.shouldBeRemoved) {
this.fish.splice(i, 1);
}
}
// if not game over, do player-related stuff
if (!this._gameOver) {
this.doPlayerUpdates();
}
// update and draw bubbles and bones
for (let i = this.bubblesAndBones.length - 1; i >= 0; --i) {
this.bubblesAndBones[i].updateAndDraw();
if (this.bubblesAndBones[i].shouldBeRemoved) {
this.bubblesAndBones.splice(i, 1);
}
}
// generate random fish
this._timeToGenerateFish -= Shaku.gameTime.delta;
if (this._timeToGenerateFish <= 0 && this.score < 5000) {
// time for next fish
this._timeToGenerateFish = 1 + Math.random() * 2.5;
if (Math.random() < 0.35) { this._timeToGenerateFish += Math.random() * 3.25; }
// create fish
let playerSize = this.player.getSize();
let size = 0.15 + Math.random() * (playerSize + 1.25);
if (Math.random() <= 0.2) { size = 0.1 + Math.random() * playerSize; }
else if (Math.random() <= 0.1) { size *= (1 + Math.random() * 1); }
size = Math.min(Math.max(size, playerSize / 2), 2.5);
let fish = new NpcFish(size);
this.fish.push(fish);
}
spritesBatch.end();
// draw foreground seaweed
spritesBatch.begin(undefined, waterEffect);
waterEffect.uniforms.elapsedTime(Shaku.gameTime.elapsed + 0.125);
waterEffect.uniforms.waveFactor(2);
let weedSize = assets.seaWeed.getSize();
spritesBatch.drawQuad(assets.seaWeed, new Shaku.utils.Vector2(350, Shaku.gfx.getCanvasSize().y), weedSize.mul(0.775), undefined, null, 0, new Shaku.utils.Vector2(0.5, 1));
spritesBatch.drawQuad(assets.seaWeed, new Shaku.utils.Vector2(650, Shaku.gfx.getCanvasSize().y), weedSize, undefined, null, 0, new Shaku.utils.Vector2(0.5, 1));
spritesBatch.drawQuad(assets.seaWeed, new Shaku.utils.Vector2(950, Shaku.gfx.getCanvasSize().y), weedSize.mul(0.5), undefined, null, 0, new Shaku.utils.Vector2(0.5, 1));
spritesBatch.end();
// blood effect overlay
spritesBatch.begin(Shaku.gfx.BlendModes.Multiply);
spritesBatch.drawRectangle(Shaku.gfx.whiteTexture, Shaku.gfx.getRenderingRegion(), undefined, new Shaku.utils.Color(1,0,0,this.messedUpFactor));
spritesBatch.end();
// draw score
spritesBatch.begin();
textSpriteBatch.begin();
spritesBatch.drawQuad(assets.skeletonIcon, new Shaku.utils.Vector2(32, 32), new Shaku.utils.Vector2(48, 48));
let scoreText = Shaku.gfx.buildText(assets.fontTexture, "x " + Math.ceil(game.score), 24, Shaku.utils.Color.white, Shaku.gfx.TextAlignments.Left);
scoreText.position.set(73, 33);
textSpriteBatch.drawText(scoreText);
spritesBatch.end();
textSpriteBatch.end();
}
doPlayerUpdates()
{
// update player
this.player.updateAndDraw();
// get player collision rect
let playerRect = this.player.getCollisionRect();
// update and draw fish
for (let i = this.fish.length - 1; i >= 0; --i) {
let fish = this.fish[i];
let fishRect = fish.getCollisionRect();
if (playerRect.collideRect(fishRect))
{
// player eat fish
if (this.player.getSize() >= fish.getSize()) {
this.fish.splice(i, 1);
this.playerEats(fish);
}
// fish eats player
else
{
this.gameOver();
}
// bite sound
Shaku.sfx.play(assets.sfx.burp, 1, 0.75 + Math.random() * 0.5, false);
}
}
}
}
// create a new game
let game = new Game();
// do a single main loop step
function step()
{
// start new frame and clear screen
Shaku.startFrame();
Shaku.gfx.clear(Shaku.utils.Color.cornflowerblue);
// update and draw game
// note: if last frame took too long, skip this frame.
// this is useful to avoid glitches when getting back from lost focus.
if (Shaku.gameTime.delta < 0.5)
{
game.updateAndDraw();
}
// restart game
if (Shaku.input.pressed('r')) {
game.stop();
game = new Game();
}
// end frame and request next frame
Shaku.endFrame();
Shaku.requestAnimationFrame(step);
}
// start main loop
step();
}
</script>
</div>
</body>
</html>