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:
    // 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);
    }
    // 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:

    // 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