Создание веб-включённой Arduino-игры «Гонки машин» в стиле Brick Game
Компоненты и материалы
| Arduino UNO | ×1 |
| PHPoC Shield для Arduino | ×1 |
| USB Wi-Fi адаптер | ×1 |
| Потенциометр (угловой датчик поворота) | ×1 |
О проекте
Демо
Эта классическая игра может вызвать ностальгию по детству. Геймплей прост: избегайте столкновений с машинами соперников и набирайте как можно больше очков. Машина может переключаться между 5 полосами, есть 15 уровней скорости. Чем выше скорость, тем быстрее едут соперники, тем сложнее игра.
Подключение
- Установите PHPoC Shield на Arduino.
- Соедините GND, VCC, SIG потенциометра с GND, 5V и A0 Arduino соответственно.

Поток данных
Arduino → PHPoC Shield → Веб-браузер
Пользователь взаимодействует с потенциометром. Значение входного сигнала используется для переключения позиции машины.
Arduino считывает значение с потенциометра. Входные ADC-значения делятся на 5 уровней. Переключение между уровнями означает смену полосы. Просто: если входной сигнал переключается на другой уровень, Arduino отправляет обновлённый уровень на PHPoC Shield.
При получении значения PHPoC WiFi Shield отправляет его в веб-браузер через WebSocket. Затем JavaScript-функция обновляет позицию (полосу) машины. JavaScript-программа непрерывно обновляет позиции соперников и скорость игры.
Что нужно сделать
- Настроить Wi-Fi для PHPoC Shield (SSID и пароль)
- Загрузить новый UI на PHPoC Shield
- Загрузить код Arduino
Настройка Wi-Fi для PHPoC Shield
См. инструкцию.
Загрузка нового веб-интерфейса на PHPoC Shield
- Скачайте исходный код PHPoC remote_racing_game.php.
- Загрузите его на PHPoC Shield через PHPoC Debugger по инструкции.
Загрузка кода Arduino
- Установите библиотеку PHPoC для Arduino (см. инструкцию).
- Загрузите код Arduino на плату.
И наконец
- Нажмите кнопку Serial в Arduino IDE, чтобы получить IP-адрес.
- Откройте браузер, введите
https://замените_на_ваш_ip/remote_racing_game.php - Нажмите кнопку Connect и играйте.
Узнайте больше о потенциометре в Arduino — Урок по потенциометру
Справочник функций
- pinMode()
- analogRead()
- digitalWrite()
- Serial.begin()
- Serial.println()
- for loop
- while loop
- if else
- loop()
- setup()
Код
Код Arduino Arduino
#include "SPI.h"
#include "Phpoc.h"
PhpocServer server(80);
int adcPin = A0;
float adcVal = 0; // переменная для хранения значения с датчика
int lastPos = -1;
float adcMin = 512-2.5*100;
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());
}
void loop() {
// когда клиент отправляет первый байт, говорим привет:
PhpocClient client = server.available();
if (client) {
adcVal = 0;
for(int i = 0; i < 50; i++) adcVal += analogRead(adcPin);
adcVal /= 50;
int pos = (int)((adcVal-adcMin) / 100);
if (pos<0){ pos = 0; }
else if (pos>4) pos =4;
if(lastPos != pos){
String txtMsg = String(pos) + "\r\n";
char buf[txtMsg.length()];
txtMsg.toCharArray(buf, txtMsg.length());
server.write(buf, txtMsg.length());
Serial.println(txtMsg);
lastPos = pos;
delayMicroseconds(50);
}
}
}
remote_racing_game.php PHP
Код для PHPoC Shield:
<!DOCTYPE html>
<html>
<head>
<title>Arduino - Web-based 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>
var cvs_width = 480, cvs_height = 400;
function object(x, y) { this.x = x; this.y = y; }
function racing_car(x, y) { this.last_x = -1; this.x = x; this.y = y; }
var ws = null;
var ctx = null;
var user_car = null;
var opponent_cars = null;
var sidelines = null;
var cur_score = null;
var level_up = null;
var cur_level = null;
var speed_default = null;
var speed_delta = null;
var updated_speed = null;
var updated_timer = null;
var is_crashed = null;
var is_not_finished = null;
var car_width = null;
var car_height = null;
var line_width = null;
var block_size = null;
var line_distance = null;
var rect_line_width = null;
var blink_times = null;
function init() {
cur_score = 0;
level_up = 0;
cur_level = 0;
speed_default = 300;
speed_delta = 20;
updated_speed = get_speed();
is_crashed = false;
is_not_finished = true;
blink_times = 0;
var width = window.innerWidth;
var height = window.innerHeight;
var ratio_x = (width - 250) / cvs_width;
var ratio_y = (height - 250) / cvs_height;
var ratio = (ratio_x < ratio_y) ? ratio_x : ratio_y;
cvs_width *= ratio;
cvs_height *= ratio;
block_size = cvs_height/24;
car_width = 3;
car_height = 4;
line_width = 2*ratio;
line_distance = 3*ratio;
rect_line_width = 1*ratio;
var canvas = document.getElementById("remote");
canvas.width = cvs_width;
canvas.height = cvs_height;
ctx = canvas.getContext("2d");
ctx.translate(100, 0);
opponent_cars = [];
for (var i = 0; i<3; i++) {
opponent_cars.push(new object((Math.floor(Math.random() * 5))*car_width, -(8+2*Math.floor(Math.random()))));
opponent_cars.push(new object((Math.floor(Math.random() * 5))*car_width, -(24+2*Math.floor(Math.random()))));
}
sidelines = [];
for (var i = 0; i<7; i++) {
sidelines.push(new object(-2, 4*i));
sidelines.push(new object(16, 4*i));
}
user_car = new racing_car(2*car_width, 20);
draw_car(user_car, false);
draw_sideline();
draw_grid();
}
function connect_onclick() {
if(ws == null) {
var ws_host_addr = "";
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 = "CONNECTED";
document.getElementById("bt_connect").innerHTML = "DISCONNECT";
ws.send("init\r\n");
updated_timer = setInterval(game_process, updated_speed);
}
function ws_onclose() {
document.getElementById("ws_state").innerHTML = "NOT CONNECTED";
document.getElementById("bt_connect").innerHTML = "CONNECT";
ws.onopen = null;
ws.onclose = null;
ws.onmessage = null;
ws = null;
clearInterval(updated_timer);
}
function ws_onmessage(e_msg) {
if (is_not_finished == true) {
e_msg = e_msg || window.event;
user_car.x = parseInt(e_msg.data)*car_width;
}
}
function get_speed() {
return (speed_default - speed_delta*cur_level)
}
function draw_score() {
ctx.font = 1.5*block_size+"px Georgia";
ctx.fillText("SCORES",18*block_size,10*block_size);
ctx.font = 1.25*block_size+"px Georgia";
ctx.fillText(""+ cur_score,20*block_size,12*block_size);
}
function draw_car(obj, is_broken) {
ctx.save();
ctx.translate(obj.x*block_size,obj.y*block_size);
if (is_broken == false) {
ctx.fillStyle = "#020202";
} else {
ctx.fillStyle = "#202020";
}
ctx.fillRect(0, block_size, 3*block_size, block_size);
ctx.fillRect(block_size, 0, block_size, 3*block_size);
ctx.fillRect(0, 3*block_size, block_size, block_size);
ctx.fillRect(2*block_size, 3*block_size, block_size, block_size);
draw_rectangle(1,0);
draw_rectangle(1,1);
draw_rectangle(1,2);
draw_rectangle(0,1);
draw_rectangle(2,1);
draw_rectangle(0,3);
draw_rectangle(2,3);
ctx.restore();
}
function draw_rectangle(x,y) {
ctx.beginPath();
ctx.lineWidth= rect_line_width;
ctx.strokeStyle="#FFFFFF";
ctx.rect(x*block_size+line_distance,y*block_size+line_distance,block_size-2*line_distance,block_size - 2*line_distance);
ctx.stroke();
}
function draw_grid() {
ctx.save();
ctx.translate(-2*block_size,0);
ctx.lineWidth = line_width;
ctx.strokeStyle = "#FCFCFC";
for( var x_id = 0; x_id <= 19; x_id++) {
ctx.beginPath();
ctx.moveTo(x_id*block_size,0);
ctx.lineTo(x_id*block_size,block_size*24);
ctx.stroke();
}
for( var y_id = 0; y_id <= 24; y_id++) {
ctx.beginPath();
ctx.moveTo(0, y_id*block_size);
ctx.lineTo(block_size*19, y_id*block_size);
ctx.stroke();
}
ctx.restore();
}
function draw_sideline() {
var move_down = 1;
if (is_not_finished == false) {
move_down = 0;
}
for( var y_id = 0; y_id < 14; y_id++) {
ctx.fillStyle = "#020202";
ctx.fillRect(sidelines[y_id].x*block_size , sidelines[y_id].y*block_size , block_size, 2*block_size);
draw_rectangle(sidelines[y_id].x,sidelines[y_id].y);
draw_rectangle(sidelines[y_id].x,sidelines[y_id].y+1);
sidelines[y_id].y += move_down;
if (sidelines[y_id].y > (user_car.y + car_height)) {
sidelines[y_id].y -= 7*car_height;
}
}
}
function move_opponent_cars() {
for (var car_id = 0; car_id < 6; car_id++) {
cur_y = opponent_cars[car_id].y;
if (cur_y <= (user_car.y + 3)) {
opponent_cars[car_id].y += 1;
} else {
cur_score += 1;
opponent_cars[car_id].x = (Math.floor(Math.random() * 5))*car_width;
opponent_cars[car_id].y += (-8*car_height+1);
}
}
level_up = Math.floor(cur_score/10);
if (level_up>cur_level) {
clearInterval(updated_timer);
cur_level = level_up;
updated_speed = get_speed();
if (updated_speed == 0) {
game_over();
return;
}
updated_timer = setInterval(game_process, updated_speed);
}
while (shuffle_needed()) {
for (var car_id = 0; car_id < 5; car_id++) {
for (var car_id_2nd = car_id+1; car_id_2nd < 6; car_id_2nd++) {
while ((opponent_cars[car_id_2nd].y == opponent_cars[car_id].y)&& (Math.abs(opponent_cars[car_id_2nd].x - opponent_cars[car_id].x)<= car_width)) {
if (Math.random()>0.5) {
opponent_cars[car_id_2nd].x = Math.floor(Math.random()* 5)*car_width;
} else {
opponent_cars[car_id_2nd].y -= Math.ceil(Math.random())*car_height;
}
}
while ((opponent_cars[car_id_2nd].x == opponent_cars[car_id].x)&& (Math.abs(opponent_cars[car_id_2nd].y - opponent_cars[car_id].y)< 2*car_height)) {
opponent_cars[car_id_2nd].y -= Math.ceil(Math.random())*2;
}
}
}
}
}
function shuffle_needed() {
for (var car_id = 0; car_id < 5; car_id++) {
for (var car_id_2nd = car_id+1; car_id_2nd < 6; car_id_2nd++) {
if ((opponent_cars[car_id_2nd].x == opponent_cars[car_id].x)&& (Math.abs(opponent_cars[car_id_2nd].y - opponent_cars[car_id].y)< 2*car_height)) return true;
if ((opponent_cars[car_id_2nd].y == opponent_cars[car_id].y)&& (Math.abs(opponent_cars[car_id_2nd].x - opponent_cars[car_id].x)<= car_width)) return true;
}
}
return false;
}
function check_crash() {
var crash_car_id = null;
for (var car_id = 0; car_id < 6; car_id++) {
cur_y = opponent_cars[car_id].y;
if ((cur_y > (user_car.y - car_height)) && (cur_y < (user_car.y + car_height)-1)) {
if (((opponent_cars[car_id].x < user_car.x) && (opponent_cars[car_id].x > user_car.last_x)) || ((opponent_cars[car_id].x>user_car.x) && (opponent_cars[car_id].x < user_car.last_x)) || (opponent_cars[car_id].x == user_car.x)) {
is_not_finished = false;
crash_car_id = car_id;
if ((Math.abs(opponent_cars[car_id].x - user_car.last_x)) <= (Math.abs(opponent_cars[crash_car_id].x - user_car.last_x))) {
crash_car_id = car_id;
user_car.x = opponent_cars[crash_car_id].x;
}
}
}
}
return (!is_not_finished);
}
function game_over() {
ctx.clearRect(-100, 0, cvs_width, cvs_height);
ctx.font = 2*block_size+ "px Georgia";
if (is_crashed == true) {
ctx.fillText("GAME OVER", 5*block_size, 10*block_size);
} else {
ctx.fillText("CONGRATULATION", 2*block_size, 10*block_size);
}
ctx.font = 1.5*block_size + "px Georgia";
ctx.fillText("SCORES", 8*block_size, 12*block_size);
ctx.font = 1.25*block_size + "px Georgia";
ctx.fillText(""+ cur_score, 10*block_size, 14*block_size);
}
function blinking() {
ctx.clearRect(-100, 0, cvs_width, cvs_height);
for (var car_id = 0; car_id < 6; car_id++) {
draw_car(opponent_cars[car_id], false);
}
draw_score();
draw_sideline();
draw_grid();
if ((blink_times % 2)==0){
draw_car(user_car, true);
}
if (blink_times > 4) {
game_over();
} else {
setTimeout(function(){blinking();}, 500);
blink_times ++;
}
}
function update_scene() {
if (is_not_finished == true) {
ctx.clearRect(-100, 0, cvs_width, cvs_height);
for (var car_id = 0; car_id < 6; car_id++) {
draw_car(opponent_cars[car_id],false);
}
draw_car(user_car,false);
draw_score();
draw_sideline();
draw_grid();
user_car.last_x = user_car.x;
}
}
function game_process() {
if(ws != null) {
is_crashed = check_crash();
if (is_crashed==true) {
clearInterval(updated_timer);
blinking();
return;
}
move_opponent_cars();
update_scene();
}
}
window.onload = init;
</script>
</head>
<body>
<center>
<p>
<h1>Arduino - Car Race </br> Brick Game</h1>
</p>
<canvas id="remote" width="500" height="400" Horizontal-align="middle";></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>
Производственный процесс
- Веб-управляемая DMX-система освещения: Arduino Mega как мастер-контроллер
- Создание классической игры Pong на Arduino UNO с OLED-дисплеем: пошаговый урок
- Сборка Bluetooth-робота-шпиона на Arduino с управлением с телефона
- DIY USB-геймпад на Arduino: собственный высокопроизводительный контроллер
- Arduino Breakout на сенсорном TFT 2.4″: классическая аркада своими руками
- Превращаем старую радиоуправляемую машину в джойстик-контролируемый болид на Arduino
- Веб-управляемый MP3-плеер на Arduino: Воспроизведение аудио через браузер
- Pixel Chaser: игра на один тап с NeoPixel Ring и Arduino Nano
- Защитите свой Arduino с помощью веб-разблокировки по шаблону — простое управление и удалённый контроль
- Автоматизация игры «Динозавр» в Chrome на Arduino и LDR с Python