Týden 10 - GUI a ovládání přes sériovou linku
O projektu
Tento týden jsme se zaměřili na vytvoření grafického uživatelského rozhraní (GUI) a ovládání aplikae přes sériovou linku. Vytvořil jsem jednoduchou hru ve stylu flappy bird, kde je třeba se vyhnout překážkám. Hru lze ovládat přes klávesnici, dotykový displej i pomocí sériové linky přes externí tlačítko připojené k arduinu.
Programování hry
V Javě jsem nikdy neprogramoval. Naštěstí processing obsahuje velké množství ukázkových programů a na internetu je mnoho tutoriálů. Nalezl jsem základní návod pro pohyb "ptáčka" a překážek a ten nadále upravoal. Následně jsem přidal zvuky a grafiku. Celé jsem se to snažil udělat ve stylu italského brainrotu. Viz odkaz
Hra je napsána v jazyce Java a běží na platformě Processing. Největším oříškem bylo nastavení pohybu překážek a jejich kolize s ptáčkem. Musel jsem si hodně pohrát s velikostí hitboxu, abych vůbec hru zvládl hrát. Hra je vymyšlena tak, že ptáček se pohybuje pouze po ose y a překážky se pohybují proti němu po ose x. Tudíž pohyb lze snadno rozložit a jako akční zásah pouze ubírat rychlost na ose y.
import processing.sound.*;
import processing.serial.*;
// object def
PImage bg;
bird b = new bird();
pillar[] p = new pillar[3];
boolean end = false; // if game should continue - end is true
boolean intro = true; // show intro once
int score = 0;
int lastScoreChange = 0;
SoundFile scoreSound;
SoundFile backgroundMusic;
boolean playScoreSound = false;
// Serial com
Serial myPort;
String serialBuffer = "";
void setup() {
size(500, 800); // creates canvas
b = new bird();
b.loadBirdImages();
bg = loadImage("bg.png"); // load images
frameRate(60); // tells processing i want 50hz
// Load sound files
scoreSound = new SoundFile(this, "point.wav"); // sound when incrementing points
backgroundMusic = new SoundFile(this, "back.mp3"); // background loop music
// start background music
backgroundMusic.loop();
backgroundMusic.amp(0.1); // make background more silent
// initialize pillars - always use 3 pillars in game
for (int i = 0; i < 3; i++) {
p[i] = new pillar(i);
}
// Initialize serial port
String portName = "COM5"; // port in use
String[] ports = Serial.list();
printArray(ports); // prints ports avivable
// checks if the selected port is connected
boolean portFound = false;
for (String port : ports) {
if (port.equals(portName)) {
portFound = true;
break;
}
}
if (portFound) { // if found start serail comunication on 9600 baud
try {
myPort = new Serial(this, portName, 9600);
myPort.bufferUntil('\n');
println("Serial port " + portName + " initialized successfully.");
} catch (Exception e) {
println("Error opening serial port " + portName + ": " + e.getMessage());
myPort = null; // Skip serial communication
}
} else {
println("Serial port " + portName + " not found. Skipping serial communication.");
myPort = null; // skip serial
}
}
void draw() {
image(bg, 0, 0); //redraw bacground
if (end) {
b.move();
}
b.drawBird();
if (end) {
b.drag();
}
b.checkCollisions();
for (int i = 0; i < 3; i++) {
p[i].drawPillar();
p[i].checkPosition();
}
// draw UI
fill(0);
stroke(255);
textSize(32);
if (end) {
rect(20, 20, 100, 50);
fill(255);
text(score, 30, 58);
} else {
rect(45, 100, 420, 50);
rect(150, 200, 200, 50);
fill(255);
if (intro) {
text("Flappy Bombardino Crocodilo", 55, 140);
text("Click to Start", 155, 240);
} else {
text("Game Over", 170, 140);
text("Score", 180, 240);
text(score, 280, 240);
}
}
fill(255);
textSize(16);
text("FPS: " + int(frameRate), 400, 30);
//plays score sound
if(playScoreSound == true){
scoreSound.amp(1);
scoreSound.play();
playScoreSound = false;
}
}
void serialEvent(Serial p) {
// Only process serial events if myPort is initialized
if (myPort != null) {
serialBuffer = p.readStringUntil('\n');
if (serialBuffer != null) {
serialBuffer = serialBuffer.trim();
if (serialBuffer.equals("jump")) {
handleJumpInput();
}
}
}
}
void handleJumpInput() {
b.jump();
intro = false;
if (end == false) {
reset();
}
}
void mousePressed() {
handleJumpInput();
}
void keyPressed() {
handleJumpInput();
}
void reset() {
end = true;
score = 0;
lastScoreChange = 0;
b.yPos = 400;
b.resetImage(); // reset to first bird image
for (int i = 0; i < 3; i++) {
p[i].xPos += 550; // reset to default position
p[i].cashed = false;
}
}
class bird {
float xPos, yPos, ySpeed;
PImage[] birdImages = new PImage[3]; // three images of bird
int currentImageIndex = 0;
bird() {
xPos = 250;
yPos = 400;
}
void loadBirdImages() {
birdImages[0] = loadImage("bird1.png");
birdImages[1] = loadImage("bird2.png");
birdImages[2] = loadImage("bird3.png");
for (PImage img : birdImages) {
img.resize(80, 80);
}
}
void changeRandomImage() { // change to random image in the array (selecting index)
int newIndex = currentImageIndex;
while (newIndex == currentImageIndex) {
newIndex = (int)random(3);
}
currentImageIndex = newIndex;
}
void resetImage() {
currentImageIndex = 0;
}
void drawBird() {
image(birdImages[currentImageIndex], xPos-40, yPos-40);
}
void jump() {
ySpeed = -10; // goes up faster
}
void drag() {
ySpeed += 0.4; // drag down - gravity accelertion (using integration of velocity)
}
void move() {
yPos += ySpeed;
for (int i = 0; i < 3; i++) {
p[i].xPos -= 3; // moving the pillars by 180px per second (assuming 60hz)
}
}
void checkCollisions() {
// Boundary check - if bird hits bottom or top
if (yPos > 800 || yPos < 0) {
end = false;
}
// if the hitbox shares boudnary with pillar
for (int i = 0; i < 3; i++) {
float dx = abs(xPos - p[i].xPos);
if (dx < 30) {
if (yPos < p[i].opening - 100 || yPos > p[i].opening + 100) {
end = false;
}
}
}
}
}
class pillar {
float xPos, opening;
boolean cashed = false; // mean - "this pillar gave point"
pillar(int i) {
xPos = 100 + (i * 200); // pillars evenly spaced
opening = random(600) + 100; // spawns opening randomly from zero to 600 px
}
void drawPillar() {
stroke(0);
strokeWeight(5);
line(xPos, 0, xPos, opening - 100);
line(xPos, opening + 100, xPos, 800);
strokeWeight(1);
}
void checkPosition() {
if (xPos < 0) {
xPos += (200 * 3);
opening = random(400) + 100;
cashed = false;
}
if (xPos < 250 && cashed == false) { // the pillars moved 250 px - score point
cashed = true; // cashed prevents scoring multiple points per passing
score++;
// Change bird every 10 points
if (score % 5 == 0 && score > lastScoreChange) {
b.changeRandomImage();
lastScoreChange = score;
}
playScoreSound = true;
}
}
}
void dispose() {
// only stop the serial port if it was initialized
if (myPort != null) {
myPort.stop();
println("Serial port closed.");
}
super.dispose();
}
Externí ovladač
Externí ovladač realizovaný tlačítkem je připojený k arduinu a komunikuje přes sériovou linku. Ovladač je naprogramován tak, že po stisknutí tlačítka odešle příkaz "jump" do počítače. Tento příkaz je zpracován v programu a ptáček skočí.
const int buttonPin = 2; // Digital pin button
bool lastButtonState = HIGH; // start with button up
void setup() {
Serial.begin(9600);
pinMode(buttonPin, INPUT_PULLUP); // set button
}
void loop() {
bool buttonState = digitalRead(buttonPin);
// Check button
if (lastButtonState == HIGH && buttonState == LOW) {
Serial.println("jump");
}
lastButtonState = buttonState;
delay(5); // for stability
}
Zapojení ovladače
Náčrt zapojení