JS Ships

Creando un multiplayer game con Vanilla JavaScript,

NodeJS y programación reactiva

V.2

CucJS

¿Que nos une?

  • Nos gusta Javascript!!!
  • Queremos llevar nuestra pasión a todos.
  • Queremos una comunidad gigantesca
  • Queremos mejorar el nivel 
  • Queremos hacerlos relevantes
  • Abrir puertas a oportunidades de investigation, entretenimiento, estudio y trabajo.

JavaScript Showcase

Web apps

NodeJS + React

NodeJS + EmberJS

NodeJS + React

AngularJS

VueJS

NodeJS + MongoDB

JavaScript Showcase

Apps multiplataforma y móviles nativas

Electron

Ionic

React Native

Electron

React Native

React Native

JavaScript Showcase

Task runners, module bundlers

y utilidades para alto rendimiento

JavaScript Showcase

V.2

JS Ships

Creando un multiplayer game con Vanilla JavaScript,

NodeJS y programación reactiva

JS Ships

¿Como surgió?

  • Charlas entretenidas
  • Posiblemente interactivas
  • Que enseñen algo nuevo
  • Y que sean interesantes (por favor 🙄)

JS Ships

¿Cual es el objetivo?

  • Mostrar de manera practica conceptos nuevos
  • Aplicación de nuevas tecnologías
  • Interactividad

JS Ships

¿Que encontraras?

  • Vanilla JS
  • Código altamente compatible
  • Pocas librerías
  • Código que puede lograr verse mas limpio y modular (es intencional 😉)

JS Ships(V1.1.0): Front-end 

function ScreenController(screenId) {
    this.canvas = document.getElementById(screenId);

    this.height = this.canvas.height;
    this.width = this.canvas.width;
    var ship = new Ship(this.height / 2, this.width / 2, this.height * 0.1)

    this.drawBackground = function (ctx) { //Draws the background grid. }

    this.render = function () {
        var ctx = this.canvas.getContext("2d"); //can be "webgl" or "webgl2"
        this.drawBackground(ctx)
        ship.draw(ctx)
    }
}
    <!DOCTYPE html>
    <html lang="en">
    <head>
        ...   
    </head>
    <body>
    
    <canvas id="my-screen" width="1000" height="1000"></canvas>
    
    <script src="assets/modules/ship/Ship.js"></script>
    <script src="assets/modules/screen/ScreenController.js"></script>
    <script src="assets/modules/main/main.js"></script>
    </body>
    </html>

JS Ships(V1.1.0): Front-end 

function Ship(x, y, size) {
    this.x = x;
    this.y = y;
    this.size = size;
    this.angle = Math.random() * 360;

    this.draw = function (ctx) { //draws a red triangle rotated "this.angle" degrees.
        ctx.beginPath();
        ctx.lineJoin = "round";

        var offset =  this.size / 2

        ctx.save(); // saves the coordinate system
        ctx.translate(this.x, this.y);
        ctx.rotate(this.angle);
        ctx.moveTo(0, -offset);
        ctx.lineTo(-offset, offset);
        ctx.lineTo(offset, offset);
        ctx.lineTo(0, -offset);

        ctx.stroke();
        ctx.fillStyle = "red";
        ctx.fill();
        ctx.restore();
    }
}

JS Ships(V1.1.0): Front-end 



    function startApp() {
    
        var screenController = new ScreenController("my-screen");
    
        var canvas = screenController.canvas;
        canvas.style.left = ((window.innerWidth / 2) - (canvas.clientWidth / 2)) + "px";
    
        screenController.render();
    }

JS Ships(V1.1.0): Back-end 




    var express = require('express')
    var app = express()
    
    console.log(process.cwd())
    app.use(express.static("src/client"))
    
    app.listen(3000, function () {
        console.log('Example app listening on port 3000!')
    })

JS Ships(V1.1.0)

JS Ships(V1.2.1): Front-end 

function startApp() {
    var screenController = new ScreenController("my-screen");

    var canvas = screenController.canvas;
    canvas.style.left = ((window.innerWidth / 2) - (canvas.clientWidth / 2)) + "px";

    screenController.render();
}
function startApp() {
    var screenController = new ScreenController("my-screen");

    var canvas = screenController.canvas;
    canvas.style.left = ((window.innerWidth / 2) - (canvas.clientWidth / 2)) + "px";

    setInterval(function () {
        screenController.render()
    }, 20)
}

JS Ships(V1.2.1): Front-end 

function ScreenController(screenId) {
    var screenController = this;

    this.canvas = document.getElementById(screenId);
    this.height = this.canvas.height;
    this.width = this.canvas.width;

    var ship = new Ship(this.height / 2, this.width / 2, this.height * 0.05);

    function notifyMoveShip(posX, posY) {
        //transform coordinates in window to coordinates in the html canvas object
        posX = posX - screenController.canvas.offsetLeft;
        posY = posY - screenController.canvas.offsetTop;

        //transform coordinates in canvas to coordinates in the canvas context.
        posX = (posX * screenController.width) / screenController.canvas.clientWidth;
        posY = (posY * screenController.height) / screenController.canvas.clientHeight;

        ship.translate(posX, posY);
    }

    function notifyShot() { console.log("shoot"); }

    this.canvas.addEventListener("mousemove", function (event) {
        notifyMoveShip(event.pageX, event.pageY);
    })
    this.canvas.addEventListener("touchmove", function (event) {
        if (event.touches.length > 1) {
            return notifyShot();
        }
        var touch = event.touches[0];
        notifyMoveShip(touch.pageX, touch.pageY);
    })
    this.canvas.addEventListener("click", notifyShot);

    //...

    this.render = function () {
        var ctx = this.canvas.getContext("2d");
        this.drawBackground(ctx);
        ship.draw(ctx);
    }
}

JS Ships(V1.2.1): Front-end 

function Ship(x, y, size) {
    this.x = x;
    this.y = y;
    this.size = size;
    this.angle = 0;

    this.draw = function (ctx) {    //...Draws the ship in ctx    }

    this.translate = function (posX, posY) {
        //transform coordinates using the middle of canvas as (0, 0)
        var tPosX = posX - this.x;
        var tPosY = -(posY - this.y);

        this.angle = -Math.atan2(tPosY, tPosX) + (90 * Math.PI / 180);
        this.x += 4*Math.sin(this.angle)
        this.y -= 4*Math.cos(this.angle)
    }

}

JS Ships(V1.2.1)

JS Ships(V1.3.1): Front-end 

function Bullet(startX, startY, angle, size) {
    var bullet = this;

    this.x = startX;
    this.y = startY;
    this.size = size;
    this.angle = angle;

    setInterval(function () {
        bullet.translate()
    }, 20)


    this.translate = function () {
        this.x += 5 * Math.sin(this.angle);
        this.y -= 5 * Math.cos(this.angle);
    };

    this.draw = function (ctx) {    //Draws a little blue triangle.   };
}

JS Ships(V1.3.1): Front-end 

function Ship(x, y, size) {
    var ship = this;

    this.x = x;
    this.y = y;
    this.size = size;
    this.angle = 0;
    this.bullets = [];
    this.canShoot = true;

    //...

    this.translate = function (posX, posY) {
        //transform coordinates using the (0, 0) point of the ship
        var tPosX = posX - this.x;
        var tPosY = -(posY - this.y);

        this.angle = -Math.atan2(tPosY, tPosX) + (90 * Math.PI / 180);
        this.x += 2 * Math.sin(this.angle);
        this.y -= 2 * Math.cos(this.angle);
    };
    this.shoot = function () {
        if (this.canShoot) {
            this.canShoot = false;
            this.bullets.push(new Bullet(this.x, this.y, this.angle, this.size * 0.4));
            setTimeout(function () {
                ship.canShoot = true;
            }, 500)
        }
    };
}

JS Ships(V1.3.1)

JS Ships(V1.4.0)

JS Ships(V1.4.0)

  • Controlador de Pantalla (ScreenController)
  • 1 Nave
  • Proyectiles
  • ¡Todo en el front!

JS Ships(V1.4.0)

JS Ships(V1.4.0)

JS Ships

Programación Reactiva

  • Módulos aislados y responsabilidades bien definidas.
  • Comportamiento que reacciona a notificaciones.

JS Ships

Programación Reactiva

(Solución convencional)

JS Ships

Programación Reactiva

JS Ships

Programación Reactiva (No patron observador)

JS Ships(V1.4.0): Front-end 

<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>

<canvas id="my-screen"></canvas>

<script src="/socket.io/socket.io.js"></script>
<script src="assets/modules/ship/Bullet.js"></script>
<script src="assets/modules/ship/Ship.js"></script>
<script src="assets/modules/screen/ScreenController.js"></script>
<script src="assets/modules/main/main.js"></script>
</body>
</html>

JS Ships(V1.4.0): Front-end 

function startApp() {
    var socket = io();

    socket.on("load-settings", function (settings) {
        var screenController = new ScreenController("my-screen", socket);

        var canvas = screenController.canvas;
        canvas.width = settings.canvas.width;
        canvas.height = settings.canvas.height;

        canvas.style.left = ((window.innerWidth / 2) - (canvas.clientWidth / 2)) + "px";

        setInterval(function () { 
            screenController.render();
        }, 20);
    });
}

JS Ships(V1.4.0): Front-end 

function ScreenController(screenId, socket) {
    var screenController = this;

    this.canvas = document.getElementById(screenId);

    this.ships = [];

    function notifyMoveShip(posX, posY) {
        //transform coordinates in window to coordinates in the html canvas object
        posX = posX - screenController.canvas.offsetLeft;
        posY = posY - screenController.canvas.offsetTop;

        //transform coordinates in canvas to coordinates in the canvas context.
        posX = (posX * screenController.canvas.width) / screenController.canvas.clientWidth;
        posY = (posY * screenController.canvas.height) / screenController.canvas.clientHeight;

        socket.emit("ship::translate", {x: posX, y: posY})
    }

    function notifyShot() {
        socket.emit("ship::shoot")
    }

    this.canvas.addEventListener("mousemove", function (event) {
        notifyMoveShip(event.pageX, event.pageY);
    });

    this.canvas.addEventListener("touchmove", function (event) {
        event.preventDefault();
        if (event.touches.length > 1) {
            return notifyShot();
        }
        var touch = event.touches[0];
        notifyMoveShip(touch.pageX, touch.pageY);
    });
    this.canvas.addEventListener("click", notifyShot);

    socket.on("update-ships-state", function (ships) {
        screenController.ships = ships;
    })

    this.drawBackground = function (ctx) {    //...    }

    this.render = function () {
        var ctx = this.canvas.getContext("2d");
        this.drawBackground(ctx)

        this.ships.forEach(function (ship) {
            ship.size = screenController.canvas.height * 0.03;
            ship.isMyShip = ship.socket_id === socket.id;

            drawShip(ctx, ship)
        })
    }
}

JS Ships(V1.4.0): Front-end 

function drawShip(ctx, ship) {
    var offset = ship.size / 2;

    ctx.save(); // saves the coordinate system

    //...

    ctx.beginPath();
    ctx.fillStyle = "blue";
    ctx.arc(0, -offset, ship.size * 0.2, 0, 2 * Math.PI);
    ctx.fill();
    ctx.stroke();

    ctx.restore();

    ship.bullets.forEach(function (bullet) {
        bullet.size = ship.size * 0.4;
        drawBullet(ctx, bullet)
    })
}

JS Ships(V1.4.0): Back-end 


var ships = []

io.on("connection", function (socket) {

    socket.emit("load-settings", settings);

    var ship = new Ship(socket);
    ships.push(ship);

    socket.on("ship::translate", function (pointerPosition) {
        ship.translate(pointerPosition.x, pointerPosition.y);
    });

    socket.on("ship::shoot", function () {
        ship.shoot();
    });

    socket.on('disconnect', function(){
        var index = ships.indexOf(ship);
        ships.splice(index, 1);
    });
})

setInterval(function () {
    io.emit("update-ships-state", ships.map(function (ship) {
        return ship.toPlainObjet()
    }))
}, 20)

JS Ships(V1.4.0): Back-end 

function Ship(socket) {
    var ship = this;

    this.socket_id = socket.id;
    this.x = 100 + (Math.random() * (settings.canvas.width - 200));
    this.y = 100 + (Math.random() * (settings.canvas.height - 200));
    this.size = settings.canvas.width * 0.05;
    this.angle = 0;
    this.bullets = [];
    this.canShoot = true;

    this.translate = function (posX, posY) {    //...    };
    this.shoot = function () {
        //...
        this.bullets.push(new Bullet(this.x, this.y, this.angle));
        //...
    };
    this.toPlainObjet = function () {
        return {
            socket_id: this.socket_id,
            x: this.x,
            y: this.y,
            angle: this.angle,
            bullets: this.bullets.map(function (bullet) {
                return bullet.toPlainObjet();
            }),
            canShoot: this.canShoot
        }
    }
}

module.exports = Ship

JS Ships(V1.4.0): Back-end 

function Bullet(startX, startY, angle) {
    var bullet = this;

    this.x = startX;
    this.y = startY;
    this.angle = angle;

    setInterval(function () {
        bullet.translate();
    }, 20)


    this.translate = function () {
        this.x += 5 * Math.sin(this.angle);
        this.y -= 5 * Math.cos(this.angle);
    };

    this.toPlainObjet = function () {
        return {
            x: this.x,
            y: this.y,
            angle: this.angle
        }
    }
}

module.exports = Bullet

JS Ships(V1.4.0)

JS Ships(v1.5.1)

Tiempo Real

JS Ships(v1.5.1)

Tiempo Real


setInterval(function () {
    io.emit("update-ships-state", ships.map(function (ship) {
        return ship.toPlainObjet()
    }))
}, 20)

JS Ships(V1.5.1): Front-end 

function ScreenController(screenId, socket) {
    var screenController = this;

    this.canvas = document.getElementById(screenId);

    this.ships = [];
    this.bullets = [];
    this.gameStatus = "playing";

    function notifyMoveShip(posX, posY) {
        //...
        socket.emit("ship::translate", {x: posX, y: posY})
    }

    function notifyShot() {
        socket.emit("ship::shoot")
    }

    /...    

    socket.on("load-current-targets", function (targets) {
        targets.forEach(function (target) {
            if (target.type === "Ship") {
                screenController.ships.push(target);
            } else {
                screenController.bullets.push(target);
            }
        });
    });


    socket.on("ship::add", function (ship) {
        ship.isMyShip = ship.socket_id === socket.id;
        screenController.gameStatus = "playing";
        screenController.ships.push(ship);
    });

    socket.on("ship::update", function (ship) {
        var existingShip = findInArray(screenController.ships, "socket_id", ship.socket_id);
        for (var key in ship) {
            existingShip[key] = ship[key];
        }
    });

    socket.on("ship::remove", function (shipSocketId) {
        var index = findIndexArray(screenController.ships, "socket_id", shipSocketId);
        var ship = screenController.ships[index];

        screenController.ships.splice(index, 1);

        if (ship.isMyShip) {
            screenController.gameStatus = "looser";
        } else if (screenController.gameStatus !== "looser" && screenController.ships.length === 1) {
            screenController.gameStatus = "winner";
        }
    });

    socket.on("bullet::add", function (bullet) {
        screenController.bullets.push(bullet);
    });

    socket.on("bullet::update", function (bullet) {
        var existingBullet = findInArray(screenController.bullets, "id", bullet.id);
        for (var key in bullet) {
            existingBullet[key] = bullet[key];
        }
    });

    socket.on("bullet::remove", function (bulletId) {
        var index = findIndexArray(screenController.bullets, "id", bulletId);
        screenController.bullets.splice(index, 1);
    });


    this.render = function () {
        /...
    }
}

JS Ships(V1.5.1)

JS Ships

Creando un multiplayer game con Vanilla JavaScript,

NodeJS y programación reactiva

Ships JS

By CucutaJS

Ships JS

  • 94