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
- Control mode selection – determines if the game is going to be controlled by head movement (for humans) or direction keys (for the robot)
- 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; } } } } } ... }
- Peripheral improvement to smoothen PVP experience, including recording the winner/loser, synchronizing game configuration, etc.