Создадим на основе Ардуино онлайн-игру для двоих игроков с использованием веб-интерфейса. Мы уже делали такую игру на основе платформы PHPoC, но для понимания создадим аналогичный проект на Ardunio.
Комплектующие
В прошлый раз игра была реализована на основе аппаратной платформы PHPoC (PHP on Chip), о которой у нас была отдельная статья. Но мы хотели показать любителям Ардуино как сделать аналогичную игру на основе этой платы Uno, которую мы будем использовать вместо PHPoC Blue.
- Arduino UNO × 1
- PHPoC WiFi шилд для Arduino × 1
- Кнопки × 4
- Макетная плата × 1
- Резистор 10 кОм × 1
Идея проекта
В игру играют два человека. каждый человек использует две кнопки для управления вратарями. Поэтому нам нужны четыре кнопки.
Arduino считывает состояния четырех кнопок. Если какая-либо из них будет нажата, Arduino пересчитает направление движения вратаря и отправит значения направления в PHPoC WiFi Shield. При получении значений PHPoC WiFi Shield отправляет его в веб-браузер через веб-сокет. Функция JavaScript будет обновлять направление движения вратарей.
Arduino -> PHPoC WiFi Shield -> Web browser
Программа JavaScript будет постоянно обновлять положение мяча, вратарей и препятствий в зависимости от их направления, а также проверять столкновение с препятствиями и вратарями.
Направление вратарей меняется в зависимости от состояния кнопок.
Обратите внимание! PHPoC Shield имеет встроенную программу для передачи данных из Arduino в веб-браузер. Поэтому нам не нужно заботиться об этом.
Схема соединения
Соединяем PHPoC Wi-Fi или PHPoC шилд к Arduino. Собираем всё согласно схеме ниже:
Что надо сделать
- Установить информацию о WiFi для щита PHPoC (SSID и пароль)
- Загрузить новый UI (пользовательский интерфейс) в PHPoC Shield
- Написать код Arduino
Настройка WiFi
Существующие шилды Arduino Ethernet и WIFI устанавливают IP-адрес и MAC-адрес в исходных кодах. В отличие от этих экранов, PHPoC WiFi Shield для Arduino предоставляет функцию, которая управляет параметрами среды, относящимися к сети самого шилда. Таким образом, его использование делает исходные коды Arduino более лаконичными.
Настроим параметры беспроводной сети в PHPoC WiFi Shield для Arduino. Для подключения к беспроводной локальной сети требуется смартфон или ноутбук.
1. Подключите PHPoC WiFi Shield для Arduino к вашему Arduino.
2. Подаем питание на Arduino.
3. Вставляем USB WIFI модем в шилд.
4. Нажмите кнопку SETUP один раз.
5. С помощью ноутбука или смартфона подключитесь к беспроводной сети, которая начинается с phpoc_.
6. Запустите веб-браузер, если он правильно подключен к беспроводной локальной сети.
7. Подключитесь к шилду, введя 192.168.0.1 в адресной строке.
8. После подключения к странице настройки (setup) вы можете настроить сетевое окружение.
9. Ниже экран страницы базовой настройки.
Загрузка веб-интерфейса в PHPoC Shield
Нам нужно сделать следующее:
- Загрузить исходный код PHPoC remote_game.php, который есть ниже.
- Загрузить код в шилд PHPoC с помощью отладчика PHPoC - PHPoC Debugger (ссылка).
Код файла remote_game.php:
<!DOCTYPE html> <html> <head> <title>PHPoC - Game</title> <meta name="viewport" content="width=device-width, initial-scale=0.7, maximum-scale=0.7"> <style> body { text-align: center; font-size: 15pt; font-family: Arial, Helvetica, sans-serif;} h1 { font-weight: bold; font-size: 25pt; } h2 { font-weight: bold; font-size: 15pt; } button { font-weight: bold; font-size: 15pt; } </style> <script> window.requestAnimFrame = (function(callback) { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60); }; })(); var cvs_width = 400, cvs_height = 500; var ball = {x:cvs_width / 2, y:cvs_height / 2 , radius:20, dir_x:1, dir_y:1, speed:3}; var obs_1 = { x: 100, y: 150, left: 0, // update later right: 0, // update later top: 0, // update later bottom: 0, // update later width: 105, height: 30, dir: 1, // up down direction speed: 2 }; var obs_2 = { x: 300, y: 350, left: 0, // update later right: 0, // update later top: 0, // update later bottom: 0, // update later width: 105, height: 30, dir: -1, // up down direction speed: 2 }; var obstacles = [obs_1, obs_2]; var keeper_1 = { x: cvs_width / 2, y: 15, left: 0, // update later right: 0, // update later top: 0, // update later bottom: 0, // update later width: 105, height: 30, dir: 0, // left right direction speed: 8 }; var keeper_2 = { x: cvs_width / 2, y: cvs_height - 15, left: 0, // update later right: 0, // update later top: 0, // update later bottom: 0, // update later width: 105, height: 30, dir: 0, // left right direction speed: 8 }; var keepers = [keeper_1, keeper_2]; var score = [0, 0]; var delay = 100; var ws = null; var ctx = null; function init() { var width = window.innerWidth; var height = window.innerHeight; var ratio_x = (width - 105) / (cvs_width); var ratio_y = (height - 200) / cvs_height; var ratio = (ratio_x < ratio_y) ? ratio_x : ratio_y; cvs_width *= ratio; cvs_height *= ratio; var canvas = document.getElementById("remote"); canvas.width = cvs_width + 105; canvas.height = cvs_height; ctx = canvas.getContext("2d"); ctx.translate(105, 0); ctx.lineWidth = 4; for( var i = 0; i < obstacles.length; i++) { obstacles[i].x *= ratio; obstacles[i].y *= ratio; obstacles[i].width *= ratio; obstacles[i].height *= ratio; obstacles[i].speed *= ratio; obstacles[i].left = obstacles[i].x - obstacles[i].width / 2; obstacles[i].right = obstacles[i].x + obstacles[i].width / 2; obstacles[i].top = obstacles[i].y - obstacles[i].height / 2; obstacles[i].bottom = obstacles[i].y + obstacles[i].height / 2; } for( var i = 0; i < keepers.length; i++) { keepers[i].x *= ratio; keepers[i].y *= ratio; keepers[i].width *= ratio; keepers[i].height *= ratio; keepers[i].speed *= ratio; keepers[i].left = keepers[i].x - keepers[i].width / 2; keepers[i].right = keepers[i].x + keepers[i].width / 2; keepers[i].top = keepers[i].y - keepers[i].height / 2; keepers[i].bottom = keepers[i].y + keepers[i].height / 2; } ball.x *= ratio; ball.y *= ratio; ball.radius *= ratio; ball.speed *= ratio; update_view(ctx); } function connect_onclick() { if(ws == null) { var ws_host_addr = "<?echo _SERVER("HTTP_HOST")?>"; ws = new WebSocket("ws://" + ws_host_addr + "/game", "text.phpoc"); document.getElementById("ws_state").innerHTML = "CONNECTING"; ws.onopen = ws_onopen; ws.onclose = ws_onclose; ws.onmessage = ws_onmessage; } else ws.close(); } function ws_onopen() { document.getElementById("ws_state").innerHTML = "<font color='blue'>CONNECTED</font>"; document.getElementById("bt_connect").innerHTML = "Disconnect"; ws.send("dummy\r\n"); } function ws_onclose() { document.getElementById("ws_state").innerHTML = "<font color='gray'>CLOSED</font>"; document.getElementById("bt_connect").innerHTML = "Connect"; ws.onopen = null; ws.onclose = null; ws.onmessage = null; ws = null; } function ws_onmessage(e_msg) { e_msg = e_msg || window.event; // MessageEvent console.log(e_msg.data); var arr = JSON.parse(e_msg.data); keepers[0].dir = parseInt(arr[0]); keepers[1].dir = parseInt(arr[1]); } function update_view(ctx) { ctx.clearRect(-105, 0, cvs_width, cvs_height); ctx.fillStyle = "black"; ctx.fillRect(0, 0, cvs_width, cvs_height); ctx.beginPath(); ctx.moveTo(-105, cvs_height / 2); ctx.lineTo(0, cvs_height / 2); ctx.stroke(); ctx.font = "120px Georgia"; ctx.textBaseline = "middle"; ctx.textAlign = "center"; var team_1 = score[0]; var team_2 = score[1]; ctx.fillStyle = "#00FF00"; ctx.fillText(team_1.toString(), -50, cvs_height / 2 - 70); ctx.fillStyle = "#0000FF"; ctx.fillText(team_2.toString(), -50, cvs_height / 2 + 50); ctx.fillStyle="#FF0000"; ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.radius, 0, 2*Math.PI); ctx.fill(); for( var i = 0; i < obstacles.length; i++) ctx.fillRect(obstacles[i].left, obstacles[i].top, obstacles[i].width, obstacles[i].height); ctx.fillStyle="#00FF00"; ctx.fillRect(keeper_1.left, keeper_1.top, keeper_1.width, keeper_1.height); ctx.fillStyle="#0000FF"; ctx.fillRect(keeper_2.left, keeper_2.top, keeper_2.width, keeper_2.height); } function collision_detect(object) { var dist_x = Math.abs(ball.x - object.x); var dist_y = Math.abs(ball.y - object.y); var TOUCH_DIST_X = ball.radius + object.width / 2; var TOUCH_DIST_Y = ball.radius + object.height / 2; if(ball.x >= object.left && ball.x <= object.right) { if(dist_y <= TOUCH_DIST_Y) { ball.dir_y *= -1; if(ball.y < object.top) ball.y = object.top - ball.radius; else if(ball.y > object.bottom) ball.y = object.bottom + ball.radius; return true; } return false; } if(ball.y >= object.top && ball.y <= object.bottom) { if(dist_x <= TOUCH_DIST_X) { ball.dir_x *= -1; if(ball.x < object.left) ball.x = object.left - ball.radius; else if(ball.x > object.right) ball.x = object.right + ball.radius; return true; } return false; } if(dist_x < TOUCH_DIST_X && dist_y < TOUCH_DIST_Y) { dist_x -= object.width / 2; //distance to corner dist_y -= object.height / 2; //distance to corner if(dist_x == dist_y) { ball.dir_x *= -1; ball.dir_y *= -1; } else if(dist_x > dist_y) ball.dir_x *= -1; else ball.dir_y *= -1; return true; } } function check_edges() { if((ball.x + ball.radius) >= cvs_width || (ball.x - ball.radius) <= 0) ball.dir_x *= -1; if((ball.y - ball.radius) >= cvs_height || (ball.y + ball.radius) <= 0) { if((ball.y - ball.radius) >= cvs_height) score[0] += 1; else score[1] += 1; ball.dir_y *= -1; ball.x = cvs_width / 2; ball.y = cvs_height / 2; delay = 100; } } function check_keepers() { for( var i = 0; i < keepers.length; i++) { var obs = keepers[i]; collision_detect(obs); } } function check_obstacles() { for( var i = 0; i < obstacles.length; i++) { var obs = obstacles[i]; collision_detect(obs); } } function move_ball() { ball.x += ball.dir_x * ball.speed; ball.y += ball.dir_y * ball.speed; } function move_obstacles() { for( var i = 0; i < obstacles.length; i++) { obstacles[i].y += obstacles[i].dir * obstacles[i].speed; if(obstacles[i].dir == 1 && obstacles[i].y > (cvs_height - 8*ball.radius)) obstacles[i].dir = -1; else if(obstacles[i].dir == -1 && obstacles[i].y < (8*ball.radius)) obstacles[i].dir = 1; obstacles[i].top = obstacles[i].y - obstacles[i].height / 2; obstacles[i].bottom = obstacles[i].y + obstacles[i].height / 2; } } function move_keepers() { for( var i = 0; i < keepers.length; i++) { keepers[i].x += keepers[i].dir*keepers[i].speed; if(keepers[i].right > cvs_width && keepers[i].dir == 1) { keepers[i].dir *= 0; keepers[i].x = cvs_width - keepers[i].width / 2 } if(keepers[i].left < 0 && keepers[i].dir == -1) { keepers[i].dir = 0; keepers[i].x = keepers[i].width / 2 } keepers[i].left = keepers[i].x - keepers[i].width / 2; keepers[i].right = keepers[i].x + keepers[i].width / 2; } } function animate(ctx) { if(ws != null) { move_keepers(); if(!delay) { move_ball(); move_obstacles(); check_edges(); check_keepers(); check_obstacles(); } else delay--; update_view(ctx); } // request new frame requestAnimFrame(function() { animate(ctx); }); } setTimeout(function() { animate(ctx); }, 100); window.onload = init; </script> </head> <body> <center> <p> <h1>PHPoC - Web-based Game</h1> </p> <canvas id="remote" width="400" height="500"></canvas> <h2> <p> WebSocket : <span id="ws_state">null</span> </p> <button id="bt_connect" type="button" onclick="connect_onclick();">Connect</button> </h2> </center> </body> </html>
При получении HTTP-запроса от веб-браузера PHPoC Shield интерпретирует PHP-скрипт в этом файле, а затем отправляет интерпретированный файл в веб-браузер.
Интерпретированный файл (содержит HTML, CSS и код JavaScript) обеспечивает интерфейс пользователя (UI), обновляет положение мяча, вратарей и препятствий в зависимости от их направлений, а также проверяет столкновения. Он также получает направление движения вратарей от websocket.
Написание кода Arduino
- Установить библиотеку PHPoC (ссылка) для Arduino (инструкция по установке библиотек).
- Загрузить код Arduino на плату.
Код для Ардуино:
#include "SPI.h" #include "Phpoc.h" PhpocServer server(80); boolean alreadyConnected = false; void setup() { Serial.begin(9600); while(!Serial) ; Phpoc.begin(PF_LOG_SPI | PF_LOG_NET); server.beginWebSocket("game"); Serial.print("WebSocket server address : "); Serial.println(Phpoc.localIP()); pinMode(6, INPUT); pinMode(7, INPUT); pinMode(8, INPUT); pinMode(9, INPUT); } int value_6 = digitalRead(6); int value_7 = digitalRead(7); int value_8 = digitalRead(8); int value_9 = digitalRead(9); int pre_dir_1 = 0; int pre_dir_2 = 0; int dir_1 = 0; int dir_2 = 0; void loop() { // when the client sends the first byte, say hello: PhpocClient client = server.available(); if (client) { value_6 = digitalRead(6); value_7 = digitalRead(7); value_8 = digitalRead(8); value_9 = digitalRead(9); dir_1 = value_7 - value_6; dir_2 = value_9 - value_8; if(dir_1 != pre_dir_1 || dir_2 != pre_dir_2) { pre_dir_1 = dir_1; pre_dir_2 = dir_2; String txtMsg = "[" + String(dir_1) + ", " + String(dir_2) + "]\r\n"; char buf[txtMsg.length()+ 1]; txtMsg.toCharArray(buf, txtMsg.length() + 1); server.write(buf, txtMsg.length()); } } }
Тестирование и итог
- Нажмите кнопку serial на Arduino IDE, чтобы увидеть IP-адрес.
- Откройте веб-браузер, введите:
http://{укажите_нужный_адрес}/remote_game.php
. - Нажмите кнопку подключения и протестируйте.
На этом всё. В следующих уроках мы подробнее поговорим об отладчике PHPoC.