Lesson 14-Sprite.js Application Practices

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 sendMessage function creates a text input for user messages. On pressing Enter, it sends the message via ws.send() and displays it using displayMessage.
  • Message Display: The displayMessage function creates a Label sprite with the message text, font, and position, adding it to the layer. The yOffset variable 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 --save

Coding

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.

Share your love