Week 12 – Progress On The Final Project

Concept

Set off with the idea of transforming my midterm project into an online PVP game from an offline PVE game (which has been achieved by this time as my major progress this week—we’ll come back to this later), the concept of my final now includes another dimension of the human-robot competition—utilizing the PVP mechanism I have now and building a physical robot to operate one of the players in the game.

Of course, technically speaking, it would be mission impossible to develop a robot that could actually play and compete with humans within a week or so. Therefore, the physical installation of the robot would be a puppet that ostensibly controls one of the players, and the actual gaming logic would still be programmed in p5.

Illustration

The bottle is a placeholder for the hand robot to be realized.

Potential Hand Robot

Based on an open-source printable robot hand 3D model, servos could be attached to the fingers’ ends to bend each of them when needed—if parameters are tuned.

p5-Arduino Communication

As the gaming logic would be done within p5, what needs to be sent to Arduino are the translated movements of the robot hand—specifically the servos, such as for how long a specific finger needs to press a key that feeds back to trigger the movement of the player in the game.

Current Progress (Code Available)

By far, based on the midterm, the following functions have been achieved

  1. Control mode selection – determines if the game is going to be controlled by head movement (for humans) or direction keys (for the robot)
  2. Synchronized online battle – realizes the real-time communication between two (or more) sketches on two (or more) computers by setting up an HTTP WAN server using Node.js and socket.io. Necessary data from one sketch is shared with the other in real time (with a buffer time of 5 ms). Core code of this part of the sketch:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    // sketch.js
    // The global broadcast dicitonaires to communicate with the other player
    let globalBroadcastGet = {
    x: 0,
    y: 0,
    rotationX: 0,
    rotationY: 0,
    rotationZ: 0,
    health: 100,
    energy: 100,
    tacticEngineOn: false,
    laserCooldown: 100, // milliseconds
    lastLaserTime: 0,
    colliderRadius: 30, // Example radius for collision detection
    destroyCountdown: 90,
    toDestroy: false,
    laserFired: 0,
    damageState: false,
    readyToPlay: false,
    bkgSelected: 'background1'
    }
    let globalBroadcastSend = {
    x: 0,
    y: 0,
    rotationX: 0,
    rotationY: 0,
    rotationZ: 0,
    health: 100,
    energy: 100,
    tacticEngineOn: false,
    laserCooldown: 100, // milliseconds
    lastLaserTime: 0,
    colliderRadius: 30, // Example radius for collision detection
    destroyCountdown: 90,
    toDestroy: false,
    laserFired: 0,
    damageState: false,
    readyToPlay: false,
    bkgSelected: 'background1'
    }
    // Interval for sending broadcasts (in milliseconds)
    const BROADCAST_INTERVAL = 5; // 5000 ms = 5 seconds
    // Setup function initializes game components after assets are loaded
    function setup() {
    ...
    // Initialize Socket.io
    socket = io();
    // Handle connection events
    socket.on('connect', () => {
    console.log('Connected to server');
    });
    socket.on('disconnect', () => {
    console.log('Disconnected from server');
    });
    // Reconnection attempts
    socket.on('reconnect_attempt', () => {
    console.log('Attempting to reconnect');
    });
    socket.on('reconnect', (attemptNumber) => {
    console.log('Reconnected after', attemptNumber, 'attempts');
    });
    socket.on('reconnect_error', (error) => {
    console.error('Reconnection error:', error);
    });
    // Listen for broadcast messages from other clients
    socket.on('broadcast', (data) => {
    // console.log('Received broadcast');s
    try {
    // Ensure the received data is a valid object
    if (typeof data === 'object' && data !== null) {
    globalBroadcastGet = data; // Replace the entire BroadcastGet dictionary
    } else {
    console.warn('Received data is not a valid dictionary:', data);
    }
    } catch (error) {
    console.error('Error processing received data:', error);
    }
    });
    // Set up the periodic sending
    setInterval(sendBroadcast, BROADCAST_INTERVAL);
    ...
    }
    // Function to send the BroadcastSend dictionary
    function sendBroadcast() {
    // Update BroadcastSend dictionary
    let BroadcastSend = globalBroadcastSend;
    // Send the entire dictionary to the server to broadcast to other clients
    socket.emit('broadcast', BroadcastSend);
    // console.log('Sent broadcast:', BroadcastSend);
    }
    // sketch.js // The global broadcast dicitonaires to communicate with the other player let globalBroadcastGet = { x: 0, y: 0, rotationX: 0, rotationY: 0, rotationZ: 0, health: 100, energy: 100, tacticEngineOn: false, laserCooldown: 100, // milliseconds lastLaserTime: 0, colliderRadius: 30, // Example radius for collision detection destroyCountdown: 90, toDestroy: false, laserFired: 0, damageState: false, readyToPlay: false, bkgSelected: 'background1' } let globalBroadcastSend = { x: 0, y: 0, rotationX: 0, rotationY: 0, rotationZ: 0, health: 100, energy: 100, tacticEngineOn: false, laserCooldown: 100, // milliseconds lastLaserTime: 0, colliderRadius: 30, // Example radius for collision detection destroyCountdown: 90, toDestroy: false, laserFired: 0, damageState: false, readyToPlay: false, bkgSelected: 'background1' } // Interval for sending broadcasts (in milliseconds) const BROADCAST_INTERVAL = 5; // 5000 ms = 5 seconds // Setup function initializes game components after assets are loaded function setup() { ... // Initialize Socket.io socket = io(); // Handle connection events socket.on('connect', () => { console.log('Connected to server'); }); socket.on('disconnect', () => { console.log('Disconnected from server'); }); // Reconnection attempts socket.on('reconnect_attempt', () => { console.log('Attempting to reconnect'); }); socket.on('reconnect', (attemptNumber) => { console.log('Reconnected after', attemptNumber, 'attempts'); }); socket.on('reconnect_error', (error) => { console.error('Reconnection error:', error); }); // Listen for broadcast messages from other clients socket.on('broadcast', (data) => { // console.log('Received broadcast');s try { // Ensure the received data is a valid object if (typeof data === 'object' && data !== null) { globalBroadcastGet = data; // Replace the entire BroadcastGet dictionary } else { console.warn('Received data is not a valid dictionary:', data); } } catch (error) { console.error('Error processing received data:', error); } }); // Set up the periodic sending setInterval(sendBroadcast, BROADCAST_INTERVAL); ... } // Function to send the BroadcastSend dictionary function sendBroadcast() { // Update BroadcastSend dictionary let BroadcastSend = globalBroadcastSend; // Send the entire dictionary to the server to broadcast to other clients socket.emit('broadcast', BroadcastSend); // console.log('Sent broadcast:', BroadcastSend); }
    // sketch.js
    
    // The global broadcast dicitonaires to communicate with the other player
    let globalBroadcastGet = {
      x: 0,
      y: 0,
      rotationX: 0,
      rotationY: 0,
      rotationZ: 0,
      health: 100,
      energy: 100,
      tacticEngineOn: false,
      laserCooldown: 100, // milliseconds
      lastLaserTime: 0,
      colliderRadius: 30, // Example radius for collision detection
      destroyCountdown: 90,
      toDestroy: false,
      laserFired: 0,
      damageState: false,
      readyToPlay: false,
      bkgSelected: 'background1'
    }
    let globalBroadcastSend = {
      x: 0,
      y: 0,
      rotationX: 0,
      rotationY: 0,
      rotationZ: 0,
      health: 100,
      energy: 100,
      tacticEngineOn: false,
      laserCooldown: 100, // milliseconds
      lastLaserTime: 0,
      colliderRadius: 30, // Example radius for collision detection
      destroyCountdown: 90,
      toDestroy: false,
      laserFired: 0,
      damageState: false,
      readyToPlay: false,
      bkgSelected: 'background1'
    }
    
    // Interval for sending broadcasts (in milliseconds)
    const BROADCAST_INTERVAL = 5; // 5000 ms = 5 seconds
    
    // Setup function initializes game components after assets are loaded
    function setup() {
      ...
    
      // Initialize Socket.io
      socket = io();
    
      // Handle connection events
      socket.on('connect', () => {
        console.log('Connected to server');
      });
    
      socket.on('disconnect', () => {
        console.log('Disconnected from server');
      });
    
      // Reconnection attempts
      socket.on('reconnect_attempt', () => {
        console.log('Attempting to reconnect');
      });
    
      socket.on('reconnect', (attemptNumber) => {
        console.log('Reconnected after', attemptNumber, 'attempts');
      });
    
      socket.on('reconnect_error', (error) => {
        console.error('Reconnection error:', error);
      });
    
      // Listen for broadcast messages from other clients
      socket.on('broadcast', (data) => {
        // console.log('Received broadcast');s
        try {
          // Ensure the received data is a valid object
          if (typeof data === 'object' && data !== null) {
            globalBroadcastGet = data; // Replace the entire BroadcastGet dictionary
          } else {
            console.warn('Received data is not a valid dictionary:', data);
          }
        } catch (error) {
          console.error('Error processing received data:', error);
        }
      });
    
      // Set up the periodic sending
      setInterval(sendBroadcast, BROADCAST_INTERVAL);
    
      ...
    }
    
    // Function to send the BroadcastSend dictionary
    function sendBroadcast() {
    
      // Update BroadcastSend dictionary
      let BroadcastSend = globalBroadcastSend;
    
      // Send the entire dictionary to the server to broadcast to other clients
      socket.emit('broadcast', BroadcastSend);
      // console.log('Sent broadcast:', BroadcastSend);
    }
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    // server.js
    /* Install socket.io and config server
    npm init -y
    npm install express socket.io
    node server.js
    */
    /* Install mkcert and generate CERT for https
    choco install mkcert
    mkcert -install
    mkcert <your_local_IP> localhost 127.0.0.1 ::1
    mv 192.168.1.10+2.pem server.pem
    mv 192.168.1.10+2-key.pem server-key.pem
    mkdir certs
    mv server.pem certs/
    mv server-key.pem certs/
    */
    const express = require('express');
    const https = require('https');
    const socketIo = require('socket.io');
    const path = require('path');
    const fs = require('fs'); // Required for reading directory contents
    const app = express();
    // Path to SSL certificates
    const sslOptions = {
    key: fs.readFileSync(path.join(__dirname, 'certs', 'server-key.pem')),
    cert: fs.readFileSync(path.join(__dirname, 'certs', 'server.pem')),
    };
    // Create HTTPS server
    const httpsServer = https.createServer(sslOptions, app);
    // Initialize Socket.io
    const io = socketIo(httpsServer);
    // Serve static files from the 'public' directory
    app.use(express.static('public'));
    // Handle client connections
    io.on('connection', (socket) => {
    console.log(`New client connected: ${socket.id}`);
    // Listen for broadcast messages from clients
    socket.on('broadcast', (data) => {
    // console.log(`Broadcast from ${socket.id}:`, data);
    // Emit the data to all other connected clients
    socket.broadcast.emit('broadcast', data);
    });
    // Handle client disconnections
    socket.on('disconnect', () => {
    console.log(`Client disconnected: ${socket.id}`);
    });
    });
    // Start HTTPS server
    const PORT = 3000; // Use desired port
    httpsServer.listen(PORT, () => {
    console.log(`HTTPS Server listening on port ${PORT}`);
    });
    // server.js /* Install socket.io and config server npm init -y npm install express socket.io node server.js */ /* Install mkcert and generate CERT for https choco install mkcert mkcert -install mkcert <your_local_IP> localhost 127.0.0.1 ::1 mv 192.168.1.10+2.pem server.pem mv 192.168.1.10+2-key.pem server-key.pem mkdir certs mv server.pem certs/ mv server-key.pem certs/ */ const express = require('express'); const https = require('https'); const socketIo = require('socket.io'); const path = require('path'); const fs = require('fs'); // Required for reading directory contents const app = express(); // Path to SSL certificates const sslOptions = { key: fs.readFileSync(path.join(__dirname, 'certs', 'server-key.pem')), cert: fs.readFileSync(path.join(__dirname, 'certs', 'server.pem')), }; // Create HTTPS server const httpsServer = https.createServer(sslOptions, app); // Initialize Socket.io const io = socketIo(httpsServer); // Serve static files from the 'public' directory app.use(express.static('public')); // Handle client connections io.on('connection', (socket) => { console.log(`New client connected: ${socket.id}`); // Listen for broadcast messages from clients socket.on('broadcast', (data) => { // console.log(`Broadcast from ${socket.id}:`, data); // Emit the data to all other connected clients socket.broadcast.emit('broadcast', data); }); // Handle client disconnections socket.on('disconnect', () => { console.log(`Client disconnected: ${socket.id}`); }); }); // Start HTTPS server const PORT = 3000; // Use desired port httpsServer.listen(PORT, () => { console.log(`HTTPS Server listening on port ${PORT}`); });
    // server.js
    
    /* Install socket.io and config server
    npm init -y
    npm install express socket.io
    node server.js
    */
    
    /* Install mkcert and generate CERT for https
    choco install mkcert
    mkcert -install
    mkcert <your_local_IP> localhost 127.0.0.1 ::1
    mv 192.168.1.10+2.pem server.pem
    mv 192.168.1.10+2-key.pem server-key.pem
    mkdir certs
    mv server.pem certs/
    mv server-key.pem certs/
    */
    
    const express = require('express');
    const https = require('https');
    const socketIo = require('socket.io');
    const path = require('path');
    const fs = require('fs'); // Required for reading directory contents
    
    const app = express();
    
    // Path to SSL certificates
    const sslOptions = {
      key: fs.readFileSync(path.join(__dirname, 'certs', 'server-key.pem')),
      cert: fs.readFileSync(path.join(__dirname, 'certs', 'server.pem')),
    };
    
    // Create HTTPS server
    const httpsServer = https.createServer(sslOptions, app);
    // Initialize Socket.io
    const io = socketIo(httpsServer);
    
    // Serve static files from the 'public' directory
    app.use(express.static('public'));
    
    // Handle client connections
    io.on('connection', (socket) => {
      console.log(`New client connected: ${socket.id}`);
    
      // Listen for broadcast messages from clients
      socket.on('broadcast', (data) => {
        // console.log(`Broadcast from ${socket.id}:`, data);
        // Emit the data to all other connected clients
        socket.broadcast.emit('broadcast', data);
      });
    
      // Handle client disconnections
      socket.on('disconnect', () => {
        console.log(`Client disconnected: ${socket.id}`);
      });
    });
    
    // Start HTTPS server
    const PORT = 3000; // Use desired port
    httpsServer.listen(PORT, () => {
      console.log(`HTTPS Server listening on port ${PORT}`);
    });

    Other changes in classes to replace local parameter passing with externally synchronized data, such as:

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    // EnemyShip.js
    class EnemyShip {
    ...
    update() {
    this.toDestroy = globalBroadcastGet.toDestroy;
    this.health = globalBroadcastGet.health;
    this.energy = globalBroadcastGet.energy;
    if (this.toDestroy === false) {
    this.x = globalBroadcastGet.x;
    this.y = globalBroadcastGet.y;
    // Update rotation based on head movement
    this.rotationX = globalBroadcastGet.rotationX;
    this.rotationY = globalBroadcastGet.rotationY;
    this.rotationZ = globalBroadcastGet.rotationZ;
    if (globalBroadcastGet.tacticEngineOn === true && this.tacticEngineUsed === false) {
    this.tacticEngine()
    }
    // Tactic engine reset
    if (this.tacticEngineOn === true) {
    let currentTime = millis();
    if (this.model === assets.models.playerShip1) {
    this.health = 100;
    } else {
    this.energy = 100;
    }
    if (currentTime - this.tacticEngineStart > 15000) {
    this.tacticEngineOn = false;
    if (this.model === assets.models.playerShip1) {
    this.health = 100;
    } else {
    this.energy = 100;
    }
    }
    }
    }
    }
    ...
    }
    // EnemyShip.js class EnemyShip { ... update() { this.toDestroy = globalBroadcastGet.toDestroy; this.health = globalBroadcastGet.health; this.energy = globalBroadcastGet.energy; if (this.toDestroy === false) { this.x = globalBroadcastGet.x; this.y = globalBroadcastGet.y; // Update rotation based on head movement this.rotationX = globalBroadcastGet.rotationX; this.rotationY = globalBroadcastGet.rotationY; this.rotationZ = globalBroadcastGet.rotationZ; if (globalBroadcastGet.tacticEngineOn === true && this.tacticEngineUsed === false) { this.tacticEngine() } // Tactic engine reset if (this.tacticEngineOn === true) { let currentTime = millis(); if (this.model === assets.models.playerShip1) { this.health = 100; } else { this.energy = 100; } if (currentTime - this.tacticEngineStart > 15000) { this.tacticEngineOn = false; if (this.model === assets.models.playerShip1) { this.health = 100; } else { this.energy = 100; } } } } } ... }
    // EnemyShip.js
    
    class EnemyShip {
      ...
    
      update() {
        this.toDestroy = globalBroadcastGet.toDestroy;
    
        this.health = globalBroadcastGet.health;
        this.energy = globalBroadcastGet.energy;
    
        if (this.toDestroy === false) {
          this.x = globalBroadcastGet.x;
          this.y = globalBroadcastGet.y;
          
          // Update rotation based on head movement
          this.rotationX = globalBroadcastGet.rotationX;
          this.rotationY = globalBroadcastGet.rotationY;
          this.rotationZ = globalBroadcastGet.rotationZ;
          
          if (globalBroadcastGet.tacticEngineOn === true && this.tacticEngineUsed === false) {
            this.tacticEngine()
          }
    
          // Tactic engine reset
          if (this.tacticEngineOn === true) {
            let currentTime = millis();
            if (this.model === assets.models.playerShip1) {
              this.health = 100;
            } else {
              this.energy = 100;
            }
            if (currentTime - this.tacticEngineStart > 15000) {
              this.tacticEngineOn = false;
              if (this.model === assets.models.playerShip1) {
                this.health = 100;
              } else {
                this.energy = 100;
              }
            }
          }
        }
      }
    
      ...
    
    }
  3. Peripheral improvement to smoothen PVP experience, including recording the winner/loser, synchronizing game configuration, etc.

Leave a Reply