Pomyślałem sobie ostatnio - "a może by tak napisać prostą grę w GAS?...". Udało się! Snake Game każdy chyba zna. Ty też brałeś/aś starą NOKIĘ 3310 od ojca żeby pograć w węża? 😅
Projekt napisany jest przy pomocy Google Apps Script, JS, HTML oraz CSS. Nie wykorzystuje tutaj żadnych dodatkowych frameworków czy bibliotek.
Węża chyba każdy na zdjęciu poniżej rozpozna prawda? Pomarańczowa kropka to pokarm. Zasady sa bardzo proste. Wężem można poruszać się po planszy oznaczonej na czarno. Po każdym zjedzeniu pokarmu, zwiększa się liczba Twoich punktów oraz długość węża. Nic przez te wszystkie lata się nie zmieniło. To cały czas ten sam snake!
Wszystko zaczęło się jak zwykle od tego miejsca, Google Apps Script IDE. Może on wyglądać tak jak na zdjęciu, ale nie musi. Ja na przykład korzystam z dodatku do Google Chrome 👉 Black Apps Script, który zamienia standardowy edytor w potężne narzędzie z ciemnym tłem - polecam 😎
Projekt składa się tym razem łącznie z 4 plików:
index.html - plansza gry
snakeCSS.html
snakeJS.html
GAS.gs
Otwierając link do aplikacji webowej, odpalana jest funkcja doGet(), znajdująca się w pliku GAS.gs, która renderuje plansze gry i pozwala na jej wyświetlenie. Wygląda tak:
function doGet(e) {
let page = "index.html";
let html = HtmlService.createTemplateFromFile(page).evaluate();
let htmlOutput = HtmlService.createHtmlOutput(html);
htmlOutput.addMetaTag('viewport', 'width=device-width, initial-scale=1');
htmlOutput.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
return htmlOutput;
}
W pliku index.html, widać wykorzystanie funkcji include(),
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
która pozwala na zamieszczenie kodu z innego pliku w trakcie jego renderowania. Dzięki tej funkcjonalności łatwiej jest utrzymać porządek i zarządzać projektem wraz z jego rozwojem. W pliku dataJS.html przechowywana jest tablica obiektów produktów wyświetlanych na głównej stronie sklepu, natomiast plik mainJS.html zawiera funkcje JavaScript, które wykonywane są po stronie klienta. W trakcie renderowania pliku index.html wszystkie 3 pliki są łączone i w rezultacie powstaje jeden spójny plik, na którego podstawie wyświetlany jest widok w aplikacji webowej.
<!DOCTYPE html>
<html>
<head>
<?!= include('snakeCSS'); ?>
<base target="_top">
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Snake Game</title>
</head>
<body>
<h1>SNAKE GAME 🐍</h1>
<p>Use keyboard arrows to play the game! Desktop only... ❌📱</p>
<div id="score-container">
<h2>Points: <span id="score">0</span></h2>
</div>
<canvas id="board"></canvas>
<center><button id="restartButton" class="restart-button" style="display: none;">🔄 Restart Game</button></center>
<?!= include('snakeJS'); ?>
</body>
<footer>
<p>© Powered by <a href="https://www.pa-ideas.com" target="_blank">PA-IDEAS</a></p>
</footer>
</html>
Odpowiedź zawarta jest w pliku snakeJS, gdzie schowana jest cała logika gry. Plik zawiera kilka funkcji oraz zmiennych. Spróbujmy zrozumieć wszystko krok po kroku...
Początkowo deklaruje kilka zmiennych
//board
var blockSize = 25;
var rows = 15;
var cols = 40;
let board;
let context;
//score
var score = 0;
//snake head
var snakeX = blockSize * 5;
var snakeY = blockSize * 5;
var velocityX = 0;
var velocityY = 0;
var snakeBody = [];
//food
let foodX;
let foodY;
var gameOver = false;
//restart button
const restartButton = document.getElementById("restartButton");
restartButton.addEventListener("click", function(url) {
google.script.run
.withSuccessHandler(function(url) {
window.open(url, '_top');
})
.getScriptURL();
})
W momencie kiedy okno z aplikacją się załaduje, wykonywana jest poniższa funkcja, która ustala wielkość planszy, rozmieszcza posiłek, nasłuchuje ruchów węża oraz ustala interwał co jaki funkcja update() powinna się wykonywać.
window.onload = function() {
board = document.getElementById("board");
board.height = rows * blockSize;
board.width = cols * blockSize;
context = board.getContext("2d"); //used for drawing on the board
placeFood();
document.addEventListener("keyup", changeDirection);
// update();
setInterval(update, 1000 / 10); //100 milliseconds
}
Funkcja update() jest najdłuższą i najbardziej złożoną funkcją w tym projekcie. Jej zadaniem jest podtrzymywać grę. Dzięki niej wąż wygląda tak jak wygląda, pokarm jest pomarańczowy i w postaci kuli, wartość punktów jest na bieżąco aktualizowana, a w przypadku popełnienia błędu rozgrywka jest zatrzymywana i pokazuje się na ekranie przycisk "Restart Game".
function update() {
if (gameOver) {
restartButton.style.display = "block";
return;
} else {
restartButton.style.display = "none";
}
const snakeHeadImage = new Image();
snakeHeadImage.src = "https://pa-ideas.com/public_files/SnakeHead_Image.png"; // Replace with your image path
snakeHeadImage.onload = function() {
context.drawImage(snakeHeadImage, snakeX, snakeY, blockSize, blockSize);
}
context.fillStyle = "black";
context.fillRect(0, 0, board.width, board.height);
context.beginPath();
context.arc(foodX + blockSize / 2, foodY + blockSize / 2, blockSize / 2, 0, 2 * Math.PI); // Draw a circle
context.fillStyle = "orange";
context.fill();
if (snakeX == foodX && snakeY == foodY) {
snakeBody.push([foodX, foodY]);
score++;
document.getElementById("score").innerHTML = score;
placeFood();
}
for (let i = snakeBody.length - 1; i > 0; i--) {
snakeBody[i] = snakeBody[i - 1];
}
if (snakeBody.length) {
snakeBody[0] = [snakeX, snakeY];
}
context.fillStyle = "lightgreen";
snakeX += velocityX * blockSize;
snakeY += velocityY * blockSize;
context.fillRect(snakeX, snakeY, blockSize, blockSize);
for (let i = 0; i < snakeBody.length; i++) {
context.fillRect(snakeBody[i][0], snakeBody[i][1], blockSize, blockSize);
}
//game over conditions
if (snakeX < 0 || snakeX > cols * blockSize || snakeY < 0 || snakeY > rows * blockSize) {
gameOver = true;
}
for (let i = 0; i < snakeBody.length; i++) {
if (snakeX == snakeBody[i][0] && snakeY == snakeBody[i][1]) {
gameOver = true;
}
}
}
Poniższe funkcje są pomocnicze. Funkcja changeDirection(e) za pomocą przekazywane parametru "e" nasłuchuje czy użytkownik wykonał ruch w lewo, prawo, górę czy dół i odpowiednio zmienia współrzędne położenia węża. Natomiast funkcja placeFood() zajmuje się umieszczaniem pokarmu w przypadkowych miejscach przy pomocy prostych obliczeń matematycznych.
function changeDirection(e) {
if (e.code == "ArrowUp" && velocityY != 1) {
velocityX = 0;
velocityY = -1;
} else if (e.code == "ArrowDown" && velocityY != -1) {
velocityX = 0;
velocityY = 1;
} else if (e.code == "ArrowLeft" && velocityX != 1) {
velocityX = -1;
velocityY = 0;
} else if (e.code == "ArrowRight" && velocityX != -1) {
velocityX = 1;
velocityY = 0;
}
}
function placeFood() {
//(0-1) * cols -> (0-19.9999) -> (0-19) * 25
foodX = Math.floor(Math.random() * cols) * blockSize;
foodY = Math.floor(Math.random() * rows) * blockSize;
}
To chyba byłoby na tyle. Mam nadzieję, że zainteresował Cię ten artykuł. Jeśli chcesz zobaczyć demo gierki, kliknij na ikonę poniżej. Aplikacja napisana jest na urządzenia desktopowe --> do nawigacji wężem używane są strzałki ⬅️ ➡️ ⬆️ ⬇️ Polecam sprobować swoich sił, zobacz jaki będzie Twój wynik 😎
Jaką gierke napisać następną? Może szachy? ♟♟♟🏁♟♟♟