Sprite.js Responsive Design
Responsive design in Sprite.js focuses on adapting 2D rendered content to various screen sizes and resolutions, ensuring a seamless user experience across devices. This document provides an in-depth analysis of implementing responsive design in Sprite.js, covering layout adjustments, auto-scaling, event handling, and more.
Layout and Container Adaptation
Using the Layout Class
Sprite.js provides the Layout class to manage element layouts, enabling automatic resizing and repositioning when the container changes.
const { Scene, Layer, Layout } = spritejs;
const scene = new Scene('#container', { width: 800, height: 600 });
const layer = scene.layer();
const layout = new Layout({
display: 'flex',
flexDirection: 'row', // Horizontal layout
justifyContent: 'center', // Center alignment
padding: 10, // Padding
});
layout.append(
new spritejs.Rect({ bgcolor: 'red', width: '50%' }),
new spritejs.Rect({ bgcolor: 'blue', width: 100, height: 100 })
);
layer.append(layout);Viewport Adaptation and Auto-Scaling
Configure the viewport and stage ratio to enable adaptive scaling.
const scene = new Scene('#container', {
width: 800,
height: 600,
autoRender: true,
resolution: [800 * window.devicePixelRatio, 600 * window.devicePixelRatio], // High-DPI support
autoResize: true, // Auto-scale to fit container
});Event Handling and Touch Response
Responsive Event Binding
Sprite.js offers a unified event handling mechanism, allowing consistent event listeners across touch and mouse inputs.
const circle = new spritejs.Circle({ r: 50, fillColor: 'red', pos: [400, 300] });
layer.append(circle);
circle.addEventListener('touchstart', (e) => {
console.log('Touch started');
});
circle.addEventListener('mousedown', (e) => {
console.log('Mouse down');
});Media Queries and Conditional Rendering
While Sprite.js doesn’t natively support CSS media queries, JavaScript logic can achieve similar effects by dynamically adjusting content based on window size.
function adjustForScreenWidth() {
const screenWidth = window.innerWidth;
if (screenWidth < 600) {
// Mobile layout
layer.children.forEach(child => child.attr({ scale: 0.8 }));
} else {
// Desktop layout
layer.children.forEach(child => child.attr({ scale: 1 }));
}
}
window.addEventListener('resize', adjustForScreenWidth);
adjustForScreenWidth();Responsive Image Handling
Prepare multiple image resolutions and dynamically select the appropriate one based on screen density or size.
const imageUrl = window.devicePixelRatio > 1 ? 'high-res-image.png' : 'low-res-image.png';
const imageSprite = new spritejs.Sprite({
texture: imageUrl,
size: [200, 200],
});
layer.append(imageSprite);Dynamic Style Adjustments
Responsive design involves dynamically updating element styles to suit different display environments. Sprite.js allows real-time style modifications via JavaScript.
function updateElementStyle() {
const isMobile = window.innerWidth <= 768;
const circle = layer.getElementById('myCircle');
if (isMobile) {
circle.attr({
fillColor: '#f00', // Red on mobile
r: 50, // Smaller radius
});
} else {
circle.attr({
fillColor: '#0f0', // Green on desktop
r: 100, // Larger radius
});
}
}
window.addEventListener('resize', updateElementStyle);
updateElementStyle();Using Percentage-Based Sizes
Use percentages instead of fixed pixels for sprite dimensions to adapt to container changes.
const responsiveSprite = new spritejs.Rect({
size: ['50%', '50%'], // 50% of container width/height
pos: ['50%', '50%'], // Centered
anchor: [0.5, 0.5], // Center anchor
fillColor: '#00f',
});
layer.append(responsiveSprite);Responsive Font Size Adjustments
For text elements, use relative units or dynamically calculate font sizes based on window dimensions for readability.
const text = new spritejs.Label({
text: 'Hello, World!',
fontSize: window.innerWidth / 20, // Scale with window width
pos: [100, 100],
});
layer.append(text);Leveraging CSS Media Queries
While Sprite.js focuses on JavaScript and WebGL, CSS media queries can complement it by controlling container styles, indirectly affecting the stage.
@media screen and (max-width: 600px) {
#container {
width: 100%;
height: auto;
}
}
@media screen and (min-width: 601px) {
#container {
width: 800px;
height: 600px;
}
}Responsive Animation Adjustments
Animations may need different speeds or effects on various screen sizes for visual harmony. Sprite.js’s animation system supports dynamic parameter adjustments.
function adjustAnimationSpeed() {
const speedMultiplier = window.innerWidth > 768 ? 1 : 0.5;
const sprite = layer.getElementById('mySprite');
sprite.animate([
{ pos: [200, 200] },
], {
duration: 1000 * speedMultiplier,
easing: 'ease-in-out',
});
}
window.addEventListener('resize', adjustAnimationSpeed);
adjustAnimationSpeed();Unified Touch and Mouse Event Handling
Unified event handling is critical for responsive design. Sprite.js’s event system ensures compatibility, but developers must ensure consistency.
const sprite = new spritejs.Rect({ size: [100, 100], pos: [400, 300], fillColor: 'blue' });
layer.append(sprite);
['touchstart', 'mousedown'].forEach(eventName => {
sprite.addEventListener(eventName, handleStartEvent);
});
function handleStartEvent(e) {
console.log('Interaction started');
}Sprite.js’s responsive design spans layout adjustments, complex interactions, and visual optimizations. By combining JavaScript logic, CSS media queries, event handling, and performance strategies, developers can create beautiful, practical cross-platform 2D applications. Prioritize user experience to ensure smooth performance across environments, and continue exploring new techniques to adapt to evolving frontend development.
Sprite.js Network Applications and Real-Time Interaction
Real-time interaction in Sprite.js network applications typically involves data communication with backend servers, user input handling, and dynamic scene updates. This document demonstrates integrating WebSocket with Sprite.js to display real-time chat messages.
Application Example
Objective
- Create a Sprite.js application to display real-time chat messages.
- Use WebSocket to connect to a backend server for sending and receiving messages.
- Dynamically add text sprites to the stage when new messages are received.
Technology Stack
- Frontend: Sprite.js, WebSocket API
- Backend: Assumes a simple WebSocket server for receiving and broadcasting messages.
Code
Start by including the Sprite.js library and setting up a basic stage.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sprite.js Real-Time Chat Example</title>
<script src="https://unpkg.com/spritejs@3/dist/spritejs.min.js"></script>
</head>
<body>
<div id="container" style="width: 800px; height: 600px;"></div>
<script src="app.js"></script>
</body>
</html>Then, implement WebSocket connection and message handling in app.js.
const { Scene, Layer, Label } = spritejs;
const scene = new Scene('#container', {
width: 800,
height: 600,
});
const layer = scene.layer();
const ws = new WebSocket('ws://your-websocket-server-url'); // Replace with actual server URL
let yOffset = 30;
ws.onopen = () => {
console.log('WebSocket connection established');
};
ws.onmessage = (event) => {
const messageData = JSON.parse(event.data);
displayMessage(messageData.text);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('WebSocket connection closed');
};
function sendMessage() {
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Enter message...';
document.body.appendChild(input);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const messageText = input.value.trim();
if (messageText) {
ws.send(JSON.stringify({ text: messageText }));
displayMessage(messageText);
input.value = '';
}
}
});
}
function displayMessage(text) {
const textNode = new Label({
text,
font: '20px Arial',
pos: [20, yOffset],
fillColor: '#000',
});
layer.append(textNode);
yOffset += 30;
}
sendMessage();Code Explanation
- WebSocket Connection: A WebSocket client connects to the server using
new WebSocket(). Handlers (onopen,onmessage,onerror,onclose) manage connection establishment, message receipt, errors, and closure. - Message Sending: The
sendMessagefunction creates a text input for user messages. On pressing Enter, it sends the message viaws.send()and displays it usingdisplayMessage. - Message Display: The
displayMessagefunction creates aLabelsprite with the message text, font, and position, adding it to the layer. TheyOffsetvariable ensures new messages stack vertically. - Real-Time Updates: New messages trigger a layer update to make content visible immediately.
Functionality Optimization
Message Scrolling and Auto-Layout
To handle growing message lists, implement scrolling and auto-layout to keep new messages visible and enhance user experience.
const maxHeight = 400;
function displayMessage(text) {
const textNode = new Label({
text,
font: '20px Arial',
pos: [20, yOffset],
fillColor: '#000',
});
layer.append(textNode);
yOffset += 30;
if (yOffset > maxHeight) {
layer.children.forEach(child => {
child.attr({ pos: [child.attr('pos')[0], child.attr('pos')[1] - 30] });
});
yOffset -= 30;
}
}Enhanced User Input and Validation
Improve input experience with focus/blur styles and message validation.
function sendMessage() {
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Enter message...';
document.body.appendChild(input);
input.addEventListener('focus', () => {
input.style.borderColor = 'blue';
});
input.addEventListener('blur', () => {
input.style.borderColor = '';
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const messageText = input.value.trim();
if (messageText.length > 200) {
alert('Message too long, keep it under 200 characters!');
return;
}
if (messageText) {
ws.send(JSON.stringify({ text: messageText }));
displayMessage(messageText);
input.value = '';
}
}
});
}Error Handling and Reconnection
Enhance WebSocket robustness with reconnection logic for unstable networks.
let reconnectTimeout;
ws.onopen = () => {
console.log('WebSocket connection established');
};
ws.onmessage = (event) => {
const messageData = JSON.parse(event.data);
displayMessage(messageData.text);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
reconnect();
};
ws.onclose = () => {
console.log('WebSocket connection closed');
reconnect();
};
function reconnect() {
clearTimeout(reconnectTimeout);
reconnectTimeout = setTimeout(() => {
ws = new WebSocket('ws://your-websocket-server-url');
ws.onopen = () => console.log('WebSocket reconnected');
ws.onmessage = (event) => {
const messageData = JSON.parse(event.data);
displayMessage(messageData.text);
};
ws.onerror = (error) => console.error('WebSocket error:', error);
ws.onclose = () => console.log('WebSocket connection closed');
}, 5000);
}Rich Message Formats
Support richer message formats like emojis or link highlighting using regex or custom sprites.
function formatMessage(text) {
const urlPattern = /(https?:\/\/[^\s]+)/g;
return text.replace(urlPattern, url => `<a href="${url}" target="_blank">${url}</a>`);
}
function displayMessage(text) {
const formattedText = formatMessage(text);
const textNode = new Label({
text: formattedText,
font: '20px Arial',
pos: [20, yOffset],
fillColor: '#000',
});
layer.append(textNode);
yOffset += 30;
}Sprite.js Performance Optimization
Performance optimization is essential when developing high-performance 2D applications with Sprite.js. Below are key strategies, illustrated with code examples, to enhance efficiency.
Minimize Redraw Areas
Reduce the area redrawn during updates to avoid frequent full-stage repaints.
// Update a specific sprite instead of the entire stage
function updateSprite(sprite) {
sprite.forceUpdate();
// Or update only the containing layer if needed
// sprite.parentLayer.forceUpdate();
}Effective Use of Layers
Group sprites into separate layers, placing static backgrounds and dynamic elements apart, as static layers avoid unnecessary redraws.
const { Scene, Layer, Rect } = spritejs;
const scene = new Scene('#container', { width: 800, height: 600 });
const backgroundLayer = scene.layer('background');
const dynamicLayer = scene.layer('dynamic');
const bgSprite = new Rect({
size: [800, 600],
pos: [0, 0],
fillColor: '#fff',
});
backgroundLayer.append(bgSprite);
const movingSprite = new Rect({
size: [50, 50],
pos: [100, 100],
fillColor: '#f00',
});
dynamicLayer.append(movingSprite);
// Update only the dynamic layer
function updateDynamicContent() {
movingSprite.attr({ pos: [movingSprite.attr('pos')[0] + 1, movingSprite.attr('pos')[1]] });
dynamicLayer.forceUpdate();
}Utilize Caching
Cache static or infrequently changing content to significantly boost performance.
const cachedSprite = new spritejs.Sprite({
texture: 'image.png',
pos: [100, 100],
});
cachedSprite.cache();
scene.layer().append(cachedSprite);Avoid Excessive Event Listeners
Too many listeners, especially for high-frequency events like mousemove, can degrade performance. Use event delegation or consolidate handlers.
// Use event delegation to reduce listeners
scene.container.addEventListener('click', (e) => {
const target = e.target.closest('.clickable');
if (target) {
// Handle click event
}
});Use requestAnimationFrame for Animations
Use requestAnimationFrame instead of setInterval or setTimeout for animations to sync with the browser’s refresh rate.
function animate() {
// Animation logic
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);Avoid Unnecessary Computations and Property Access
In loops or frequently called functions, avoid redundant computations and property accesses.
// Bad: Repeated property access in loop
for (let i = 0; i < 100; i++) {
const pos = sprite.attr('pos');
// Use pos
}
// Good: Access once and reuse
const pos = sprite.attr('pos');
for (let i = 0; i < 100; i++) {
// Use pos
}Use Web Workers for Intensive Computations
Offload complex computations to Web Workers to prevent blocking UI rendering.
// worker.js
self.addEventListener('message', (e) => {
const data = e.data;
// Perform heavy computation
const result = performHeavyComputation(data);
self.postMessage(result);
});
// Main thread
const worker = new Worker('worker.js');
worker.postMessage(someData);
worker.onmessage = (e) => {
const computedResult = e.data;
// Update UI with result
};Image Preloading and Lazy Loading
Preload images to reduce loading delays, and use lazy loading for non-critical content.
const imagesToLoad = ['image1.png', 'image2.png', 'image3.png'];
const loadedImages = {};
function preloadImages() {
imagesToLoad.forEach((src) => {
const img = new Image();
img.src = src;
img.onload = () => {
loadedImages[src] = img;
};
});
}
preloadImages();
// Use loaded images
function createSpriteWithImage(src) {
if (loadedImages[src]) {
const sprite = new spritejs.Sprite({
texture: loadedImages[src],
pos: [100, 100],
});
scene.layer().append(sprite);
} else {
console.error('Image not loaded yet');
}
}Optimize Resource Sizes
Minimize resource sizes through image compression, using SVGs instead of bitmaps where appropriate, and code minification to reduce load times and memory usage.
// Assume a tool or library for compression
const optimizedImage = compressImage(originalImage);Prevent Memory Leaks
Promptly clean up unused objects, especially in long-running applications, to avoid performance degradation from memory leaks.
// Remove and destroy unused sprites
function removeAndDestroySprite(sprite) {
sprite.remove();
}Conclusion
Performance optimization is a multifaceted effort requiring tailored strategies for different scenarios. By applying the above techniques and examples, developers can enhance Sprite.js application performance, ensuring smooth experiences across devices and network conditions. Regular performance evaluations and testing are crucial to identify and resolve emerging issues promptly.
Sprite.js Project Practice
Project Preparation
- Environment Setup: Ensure Node.js is installed. Use npm to install Sprite.js and its dependencies.
- Project Structure: Create a project folder, initialize an npm project, and install Sprite.js.
mkdir space-shooter
cd space-shooter
npm init -y
npm install spritejs --saveCoding
Basic Layout and Responsive Design
Create index.html and app.js. In index.html, include Sprite.js and set up the HTML structure. In app.js, initialize the stage with a responsive layout.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Space Shooter</title>
<style>
#container {
width: 100%;
height: 100vh;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="container"></div>
<script src="node_modules/spritejs/dist/spritejs.min.js"></script>
<script src="app.js"></script>
</body>
</html>// app.js
const { Scene, Layer, Sprite } = spritejs;
const scene = new Scene('#container', {
width: 800,
height: 600,
autoResize: true,
});
const layer = scene.layer();
// Create responsive background
const bg = new Sprite({
size: [800, 600],
pos: [0, 0],
bgcolor: '#000',
});
layer.append(bg);
// Adjust stage size on window resize
window.addEventListener('resize', () => {
scene.attr({
width: window.innerWidth,
height: window.innerHeight,
});
bg.attr({ size: [window.innerWidth, window.innerHeight] });
});Adding the Player Spaceship
Create a player spaceship sprite with basic keyboard controls.
let playerShip;
const playerSpeed = 5;
function initPlayer() {
playerShip = new Sprite({
texture: 'playerShip.png',
anchor: [0.5, 0.5],
pos: [scene.attr('width') / 2, scene.attr('height') - 100],
scale: 0.5,
});
layer.append(playerShip);
document.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowLeft':
playerShip.attr({ x: playerShip.attr('x') - playerSpeed });
break;
case 'ArrowRight':
playerShip.attr({ x: playerShip.attr('x') + playerSpeed });
break;
}
});
}
initPlayer();Enemy Generation and Collision Detection
Generate enemy sprites periodically and detect collisions with the player spaceship.
const enemySpeed = 2;
const enemies = [];
let generateEnemyInterval;
function generateEnemy() {
const enemy = new Sprite({
texture: 'enemyShip.png',
anchor: [0.5, 0.5],
pos: [Math.random() * scene.attr('width'), 0],
scale: 0.3,
});
enemies.push(enemy);
layer.append(enemy);
enemy.animate([
{ y: scene.attr('height') },
], {
duration: scene.attr('height') / enemySpeed * 1000,
easing: 'linear',
fill: 'forwards',
}).finished.then(() => {
const index = enemies.indexOf(enemy);
if (index > -1) enemies.splice(index, 1);
enemy.remove();
});
}
generateEnemyInterval = setInterval(generateEnemy, 2000);
function checkCollision() {
enemies.forEach((enemy) => {
const dx = playerShip.attr('x') - enemy.attr('x');
const dy = playerShip.attr('y') - enemy.attr('y');
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
console.log('Game Over!');
clearInterval(generateEnemyInterval);
}
});
}
setInterval(checkCollision, 100);Adding Shooting Functionality
Enable the player spaceship to fire bullets, handling their movement and removal.
const bulletSpeed = 10;
const bullets = [];
let canShoot = true;
function shoot() {
if (!canShoot) return;
canShoot = false;
const bullet = new Sprite({
texture: 'bullet.png',
anchor: [0.5, 0.5],
pos: [playerShip.attr('x'), playerShip.attr('y')],
scale: 0.1,
});
bullets.push(bullet);
layer.append(bullet);
bullet.animate([
{ y: -50 },
], {
duration: (scene.attr('height') - bullet.attr('y')) / bulletSpeed * 1000,
easing: 'linear',
fill: 'forwards',
}).finished.then(() => {
const index = bullets.indexOf(bullet);
if (index > -1) bullets.splice(index, 1);
bullet.remove();
canShoot = true;
});
}
document.addEventListener('keydown', (e) => {
if (e.key === ' ') {
shoot();
}
});Bullet and Enemy Collision Detection
Add bullet-enemy collision detection to the checkCollisions function to destroy enemies.
function checkCollisions() {
// Player-enemy collision
enemies.forEach((enemy) => {
const dx = playerShip.attr('x') - enemy.attr('x');
const dy = playerShip.attr('y') - enemy.attr('y');
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
console.log('Game Over!');
clearInterval(generateEnemyInterval);
}
});
// Bullet-enemy collision
bullets.forEach((bullet, bulletIndex) => {
enemies.forEach((enemy, enemyIndex) => {
const dx = bullet.attr('x') - enemy.attr('x');
const dy = bullet.attr('y') - enemy.attr('y');
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 50) {
enemies.splice(enemyIndex, 1);
enemy.remove();
bullets.splice(bulletIndex, 1);
bullet.remove();
}
});
});
}Scoring System
Add a scoring system for destroying enemies and display the score on the screen.
let score = 0;
let scoreLabel;
function updateScore() {
if (scoreLabel) scoreLabel.remove();
scoreLabel = new spritejs.Label({
text: `Score: ${score}`,
font: '20px Arial',
pos: [10, 10],
fillColor: '#fff',
});
layer.append(scoreLabel);
}
function checkCollisions() {
// Player-enemy collision
enemies.forEach((enemy) => {
const dx = playerShip.attr('x') - enemy.attr('x');
const dy = playerShip.attr('y') - enemy.attr('y');
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
console.log('Game Over!');
clearInterval(generateEnemyInterval);
gameOver();
}
});
// Bullet-enemy collision
bullets.forEach((bullet, bulletIndex) => {
enemies.forEach((enemy, enemyIndex) => {
const dx = bullet.attr('x') - enemy.attr('x');
const dy = bullet.attr('y') - enemy.attr('y');
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 50) {
score++;
updateScore();
enemies.splice(enemyIndex, 1);
enemy.remove();
bullets.splice(bulletIndex, 1);
bullet.remove();
}
});
});
}Game Over and Restart
Implement game over logic and a restart option when conditions are met (e.g., player collision).
function gameOver() {
const gameOverLabel = new spritejs.Label({
text: 'Game Over!',
font: '40px Arial',
pos: [scene.attr('width') / 2, scene.attr('height') / 2],
fillColor: '#f00',
anchor: [0.5, 0.5],
});
layer.append(gameOverLabel);
const restartButton = new spritejs.Label({
text: 'Restart',
font: '20px Arial',
pos: [scene.attr('width') / 2, scene.attr('height') / 2 + 50],
fillColor: '#00f',
anchor: [0.5, 0.5],
});
layer.append(restartButton);
restartButton.addEventListener('click', () => {
score = 0;
enemies.length = 0;
bullets.length = 0;
layer.removeChildren();
initPlayer();
updateScore();
generateEnemyInterval = setInterval(generateEnemy, 2000);
});
}Dynamic Difficulty Adjustment
Adjust enemy spawn rate, quantity, or behavior based on game progress to increase challenge.
let difficultyLevel = 1;
const difficultyThresholds = [10, 20, 30];
function adjustDifficulty() {
if (difficultyThresholds.includes(score)) {
difficultyLevel++;
clearInterval(generateEnemyInterval);
generateEnemyInterval = setInterval(generateEnemy, 2000 / difficultyLevel);
}
}
// Call after score updates
function updateScore() {
if (scoreLabel) scoreLabel.remove();
scoreLabel = new spritejs.Label({
text: `Score: ${score}`,
font: '20px Arial',
pos: [10, 10],
fillColor: '#fff',
});
layer.append(scoreLabel);
adjustDifficulty();
}Sound Effects Integration
Add sound effects to enhance immersion, assuming audio files are available.
const shootSound = new Audio('shoot.wav');
const explosionSound = new Audio('explosion.wav');
function playShootSound() {
shootSound.currentTime = 0;
shootSound.play();
}
function playExplosionSound() {
explosionSound.currentTime = 0;
explosionSound.play();
}
function shoot() {
if (!canShoot) return;
canShoot = false;
playShootSound();
const bullet = new Sprite({
texture: 'bullet.png',
anchor: [0.5, 0.5],
pos: [playerShip.attr('x'), playerShip.attr('y')],
scale: 0.1,
});
bullets.push(bullet);
layer.append(bullet);
bullet.animate([
{ y: -50 },
], {
duration: (scene.attr('height') - bullet.attr('y')) / bulletSpeed * 1000,
easing: 'linear',
fill: 'forwards',
}).finished.then(() => {
const index = bullets.indexOf(bullet);
if (index > -1) bullets.splice(index, 1);
bullet.remove();
canShoot = true;
});
}
function checkCollisions() {
enemies.forEach((enemy) => {
const dx = playerShip.attr('x') - enemy.attr('x');
const dy = playerShip.attr('y') - enemy.attr('y');
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
gameOver();
}
});
bullets.forEach((bullet, bulletIndex) => {
enemies.forEach((enemy, enemyIndex) => {
const dx = bullet.attr('x') - enemy.attr('x');
const dy = bullet.attr('y') - enemy.attr('y');
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 50) {
score++;
updateScore();
playExplosionSound();
enemies.splice(enemyIndex, 1);
enemy.remove();
bullets.splice(bulletIndex, 1);
bullet.remove();
}
});
});
}Mobile Optimization
Ensure the game runs well on mobile devices with touch controls, screen adaptation, and performance tweaks.
Touch Events: Replace keyboard controls with touch inputs.
let touchStartX;
scene.container.addEventListener('touchstart', (e) => {
e.preventDefault();
touchStartX = e.touches[0].clientX;
playerShip.attr('x', touchStartX);
});
scene.container.addEventListener('touchmove', (e) => {
e.preventDefault();
playerShip.attr('x', e.touches[0].clientX);
});
scene.container.addEventListener('touchend', () => {
canShoot = true;
});
scene.container.addEventListener('touchstart', () => {
shoot();
});Screen Adaptation: Auto-scale elements based on screen size.
scene.attr({ resolution: [800 * window.devicePixelRatio, 600 * window.devicePixelRatio] });Performance Optimization: On mobile, minimize sprite counts, reduce texture quality, or use efficient animations.
User Interface Improvements
- Health Display: Show player health or shield bar.
- Pause/Resume: Add pause and resume functionality.
- Settings Menu: Allow volume adjustments, fullscreen toggling, etc.



