2024 06 04 implementing background scrolling in react game project with character movements - bizkite-co/handterm GitHub Wiki


original file name: Codeium Chat - 2LzsOxNmtJfVlcCu7goWpvmbDwTCBqwz.md summary: This chat primarily discusses implementing a background scrolling feature in a game project using React TypeScript. Here is a summary of the key points discussed:

  1. Background Repeating: A solution was provided to ensure the background image repeats smoothly by using the createPattern() method in the drawBackground function.

  2. Character Movement: Adjusting the hero's movement relative to the canvas and stopping it when the background scrolls involved passing an inScrollMode prop from CharacterActionComponent to BaseCharacter.

  3. Preloading Background: To avoid gaps when scrolling, preloading background segments beyond the visible canvas area was suggested by modifying the drawBackground method.

  4. Reducing Lag: Various strategies were proposed to reduce the lag before the hero reaches the scrolling threshold, including optimizing state updates and using immediate reads.

  5. Zombie Movement: A system for allowing zombies to scroll with the background while maintaining their walk motion relative to the background was discussed, involving updating their positions accordingly in relation to the background offset.

Overall, the chat mainly focused on addressing issues related to background scrolling and character interactions in a game project, providing code snippets and suggestions to enhance the game performance and user experience. date: 2024-06-04T09:52

Codeium Chat Conversation

Note: This is purely the output of the chat conversation and does not contain any raw data, codebase snippets, etc. used to generate the output.

User at 9:52 am, Jun 4, 2024

My background scrolling is working but the background pattern runs out of image. When I move it to the left, the background becomes blank.

How can I extend the background to keep repeating?

 updateCharacterAndBackground() {
  const canvasCenterX = this.props.canvasWidth / 2;
  const characterReachThreshold = canvasCenterX; // Character stays in the middle

  if (!this.hero) return;
  // Get the current horizontal movement from the hero's currentAction state
  const heroDx = this.hero.getCurrentAction().dx; // Assuming this.state.heroAction.dx exists

  // Update character position as usual
  const newHeroPositionX = this.state.heroPosition.leftX + heroDx;

  // Check if the hero reaches the threshold

  if (newHeroPositionX >= characterReachThreshold) {
   console.log("Character reached the threshold", newHeroPositionX, characterReachThreshold);
   // Stop the hero's horizontal movement at the threshold
   this.setState({ heroPosition: { ...this.state.heroPosition, leftX: characterReachThreshold } });

   // Update the background position
   this.updateBackgroundPosition(this.state.backgroundOffsetX + heroDx);
  } else {
   // Update the hero's position normally
   this.setState({ heroPosition: { ...this.state.heroPosition, leftX: newHeroPositionX } });
  }

  // Update zombie positions relative to the backgroundOffsetX
  // Assuming you have a method to update zombies
  // this.updateZombiesPosition();
 }

Codeium at 9:52 am, Jun 4, 2024

To ensure the background image keeps repeating and does not run out, you should use a looping pattern for the background. This is typically done using the createPattern() method of the CanvasRenderingContext2D, which you are likely using in the drawBackground method.

Make sure that you set the pattern as the fillStyle and then fill the canvas appropriately. Here's an example based on the code in {TerminalGame.tsx:drawBackground}:

drawBackground(context: CanvasRenderingContext2D) {
  // ...
  const pattern = context.createPattern(this.bgImage, 'repeat');
  if (pattern) {
    context.fillStyle = pattern;
    context.save(); // Save the current context state
    context.translate(-this.state.backgroundOffsetX % this.bgImage.width, 0); // Correct the offset
    context.fillRect(0, 0, this.props.canvasWidth, this.props.canvasHeight);
    context.restore(); // Restore the context state
  } else {
    // Handle the null pattern case
    // ...
  }
  // ...
}

In the translate call, the modulo % operator is used with the background image's width to create a seamless looping effect when the background scrolls. This way, when backgroundOffsetX exceeds the width of the image, it wraps around, creating an infinite scrolling effect. Adjust your drawBackground method accordingly to ensure the background repeats infinitely.


User at 10:01 am, Jun 4, 2024

Ok, that works.

I have another problem. the x motion is handled by the BaseCharacter, but when we go into background scroll mode, I want the Hero to stop moving relative to the canvas. I want the background to move instead.

SHould I pass a inScrollMode prop to the CharacterActionComponent and then pass that through updatePositionAndAnimate to the BaseCharacter?


Codeium at 10:01 am, Jun 4, 2024

Yes, passing an inScrollMode prop to CharacterActionComponent and then through to BaseCharacter is a clean and straightforward way to handle this logic. You can use this prop to conditionally adjust the character's movement relative to the canvas.

First, you can add the inScrollMode prop to the ICharacterActionComponentProps interface in {CharacterActionComponent.tsx}:

interface ICharacterActionComponentProps {
  // ... other props
  inScrollMode: boolean;
  // ...
}

Then, you can modify the CharacterActionComponent to use this new prop and pass it through to BaseCharacter.updatePositionAndAnimate:

export const CharacterActionComponent: React.FC<ICharacterActionComponentProps> = (
  props: ICharacterActionComponentProps
) => {
  useEffect(() => {
    // ... existing code

    // Pass the inScrollMode prop to the updatePositionAndAnimate method
    props.baseCharacter.updatePositionAndAnimate(props.onPositionChange, props.canvasWidth, props.inScrollMode);

    // ... existing cleanup code
  }, [props.currentActionType, props.baseCharacter, props.onPositionChange, props.canvasWidth, props.inScrollMode]);

  // ... existing component logic
};

In the BaseCharacter class, you can update the updatePositionAndAnimate method to accept the inScrollMode parameter and use it to conditionally update the character's position:

public updatePositionAndAnimate(
  callback: (newPosition: SpritePosition) => void,
  canvasWidth: number,
  inScrollMode: boolean
) {
  const animate = () => {
    // ... existing code to calculate the new position

    if (!inScrollMode) {
      // Update the character's position if not in scroll mode
      this.position.leftX = /* ... */
    }

    // ... existing drawing and animation frame logic
  };

  // ... existing initialization logic for the animation loop
}

Finally, in {TerminalGame.tsx}, you can pass the inScrollMode prop to CharacterActionComponent based on the condition when the background should be moving instead of the Hero:

<CharacterActionComponent
  // ... other props
  inScrollMode={newHeroPositionX >= characterReachThreshold}
/>

This way, when the Hero reaches the threshold and the background starts scrolling, the inScrollMode prop will be true, and BaseCharacter will know not to move the Hero relative to the canvas.


User at 10:20 am, Jun 4, 2024

THis is working:

  if (pattern) {
    context.fillStyle = pattern;
    context.save(); // Save the current context state
    context.translate(-this.state.backgroundOffsetX % this.bgImage.width, 0); // Correct the offset
    context.fillRect(0, 0, this.props.canvasWidth, this.props.canvasHeight);
    context.restore(); // Restore the context state
  } else {

But the new background image segments don't get loaded until there is a significant gap.

Is there any way to load background beyond the convas so that it scolls in while the existing background moves out?


Codeium at 10:20 am, Jun 4, 2024

To prevent the gap from appearing, you can preload the background segments beyond the visible canvas area so that they are ready to scroll in. A common approach is to draw a larger area than the visible canvas, ensuring that the next part of the background is already rendered off-screen and scrolls smoothly into view.

You can adjust the drawBackground method to draw the background twice side by side when nearing the edge of the image. Here's how you might modify the method:

drawBackground(context: CanvasRenderingContext2D) {
  context.globalAlpha = 0.6; // Set to desired transparency level (0 to 1)
  const pattern = context.createPattern(this.bgImage, 'repeat');
  if (pattern) {
    context.fillStyle = pattern;
    context.save();
    // Calculate the x-offset for the repeating pattern
    const offsetX = -this.state.backgroundOffsetX % this.bgImage.width;
    context.translate(offsetX, 0);
    // Draw the pattern twice if near the edge to cover the entire canvas plus extra space
    context.fillRect(offsetX, 0, this.props.canvasWidth - offsetX, this.props.canvasHeight);
    context.fillRect(this.bgImage.width + offsetX, 0, this.props.canvasWidth - offsetX, this.props.canvasHeight);
    context.restore();
  } else {
    // Handle the null pattern case, perhaps by filling a solid color or logging an error
    context.fillStyle = 'grey'; // Fallback color
    context.fillRect(0, 0, this.props.canvasWidth, this.props.canvasHeight);
  }
  context.globalAlpha = 1; // Reset to full opacity
}

In this code, context.fillRect is called twice when the offsetX is such that the end of the image would be visible on the canvas, which happens when offsetX is negative and its absolute value is less than this.props.canvasWidth. The first fillRect call renders the main part of the background, and the second fillRect ensures that there is no gap by drawing the continuation of the pattern.

Please adjust the logic based on your specific requirements and ensure that the background segments load and display correctly.


User at 10:26 am, Jun 4, 2024

In here:

 updateCharacterAndBackground() {
  const canvasCenterX = this.props.canvasWidth / 2;
  const characterReachThreshold = canvasCenterX; // Character stays in the middle

  if (!this.hero) return;
  // Get the current horizontal movement from the hero's currentAction state
  const heroDx = this.hero.getCurrentAction().dx; // Assuming this.state.heroAction.dx exists

  // Update character position as usual
  const newHeroPositionX = this.state.heroPosition.leftX + heroDx;

  // Check if the hero reaches the threshold

  if (newHeroPositionX >= characterReachThreshold) {
   this.isInScrollMode = true;
   // Stop the hero's horizontal movement at the threshold
   this.setState({ heroPosition: { ...this.state.heroPosition, leftX: characterReachThreshold } });

   // Update the background position
   this.updateBackgroundPosition(this.state.backgroundOffsetX + heroDx);
  } else {
   // Update the hero's position normally
   this.setState({ heroPosition: { ...this.state.heroPosition, leftX: newHeroPositionX } });
  }

  // Update zombie positions relative to the backgroundOffsetX
  // Assuming you have a method to update zombies
  // this.updateZombiesPosition();
 }

There is a significant lag before threshold is triggered, so the hero ends up on the right side of the screen.

What is the easiest way to reduce that lag significantly?


Codeium at 10:27 am, Jun 4, 2024

The lag before the threshold is triggered could be due to several factors. Here are some common issues to check and ways to reduce lag:

  1. Asynchronous State Updates: Remember that setState is asynchronous. If the game relies on the updated state to be reflected immediately, consider using the functional form of setState that provides the previous state as an argument, or manage the state in a more synchronous manner.

  2. Heavy Rendering: Ensure that the drawBackground and character drawing functions are optimized and not causing the main thread to be blocked.

  3. Throttling setState Calls: If setState is being called too frequently, it can cause performance issues. Throttle the calls to setState for the background and character positions if needed.

  4. Animation Loop: Make sure that the game's animation loop (requestAnimationFrame) is functioning correctly and that the updateCharacterAndBackground method is being called consistently on each frame.

To reduce the lag, consider the following adjustments:

  • Optimize updateCharacterAndBackground: Reduce the complexity inside this function. If there are any non-critical calculations or operations, move them outside the animation loop.

  • Use a Ref for Immediate Reads: Instead of relying on this.state for calculations within the animation loop, use a ref, which is updated synchronously.

  • Batch setState Calls: If you have multiple setState calls that can be batched together, do so to reduce the number of re-renders.

  • Profile the Game: Use the Performance tab in Chrome DevTools or a similar profiler to identify bottlenecks in the game loop.

Here's an example of using a ref for immediate reads:

class TerminalGame extends React.Component {
  // ...
  heroPositionX = this.props.canvasWidth / 2; // Use a class property or ref for immediate reads

  updateCharacterAndBackground() {
    // ...
    const newHeroPositionX = this.heroPositionX + heroDx;

    if (newHeroPositionX >= characterReachThreshold) {
      this.isInScrollMode = true;
      this.heroPositionX = characterReachThreshold; // Update the position synchronously
      this.updateBackgroundPosition(this.state.backgroundOffsetX + heroDx);
      // Use functional form of setState to ensure heroPosition is updated based on current state
      this.setState(prevState => ({
        heroPosition: { ...prevState.heroPosition, leftX: this.heroPositionX }
      }));
    } else {
      this.heroPositionX = newHeroPositionX; // Update the position synchronously
      this.setState(prevState => ({
        heroPosition: { ...prevState.heroPosition, leftX: this.heroPositionX }
      }));
    }
    // ...
  }
  // ...
}

By using a class property or ref, you can read and write the position synchronously, ensuring the hero's position is always up to date for the current frame. The setState call updates the state for the next render, but the immediate reads during the animation loop use the ref or class property directly. This can help reduce lag caused by waiting for state updates.


User at 11:01 am, Jun 4, 2024

Ok, my background is scrolling well.

I am having trouble with Zombie4.

I want the zombie and everything other than the Hero (I will be adding additional zombies, etc) to scroll with the background when the background scrolls, but I still want the zombie to have a walk motion when we are not scrolling, and even while we are scrolling, but the Walk action for the zombie should be relative to the background image and not the canvas.

How can I build such a system?


Codeium at 11:02 am, Jun 4, 2024

To build a system where the Zombie4 (and other entities) scroll with the background but also have their own walk motion, you can follow these steps:

  1. Scrolling with Background: When in scroll mode, update the position of Zombie4 and other entities relative to the backgroundOffsetX. This makes them move with the background.

  2. Independent Walk Motion: When not in scroll mode, or even while scrolling, update the position of Zombie4 based on its own walk motion. This walk motion should be independent of the canvas but should take into consideration the background position to give the illusion that the zombie is walking on the background.

  3. Action System: Define actions like Walk, Run, etc., for Zombie4, which should include both the animation frames and the logic for moving the sprite.

Here's an example of how to implement this system:

In the BaseCharacter or Zombie4 class, implement a method to handle the walk motion:

// BaseCharacter.tsx or Zombie4.tsx
public walk(deltaTime: number) {
  // Assuming you have a velocity or speed property
  this.position.leftX += this.velocity.dx * deltaTime;
  // ... handle the Y-axis if needed
}

In your game loop, or wherever you update entities' positions, add logic to handle both the background scrolling and the walk motion:

// TerminalGame.tsx
updateEntities(deltaTime: number) {
  // Update the zombie position
  if (this.isInScrollMode) {
    // Scroll with the background
    this.zombie4.position.leftX -= this.hero.getCurrentAction().dx * deltaTime;
  }
  // Walk motion should always be applied, but it's relative to the background
  this.zombie4.walk(deltaTime);

  // Update the state to re-render the component with the new positions
  this.setState({ zombie4Position: this.zombie4.position });
  
  // ... include similar logic for any other entities
}

Finally, in your main game loop, call this updateEntities method:

// TerminalGame.tsx
startAnimationLoop(context: CanvasRenderingContext2D) {
  const loop = (timestamp: number) => {
    // ... existing code ...

    // Calculate deltaTime since the last frame
    const deltaTime = timestamp - this.lastFrameTime;
    this.lastFrameTime = timestamp;

    // Update characters and background
    this.updateCharacterAndBackground();

    // Update other entities like Zombie4
    this.updateEntities(deltaTime);

    // ... existing code to draw the entities ...
    requestAnimationFrame(loop);
  };
  
  this.lastFrameTime = performance.now();
  requestAnimationFrame(loop);
}

This system allows the Zombie4 to move independently of the background when not in scroll mode and to move with the background when in scroll mode. The walk method controls the zombie's own motion, while the updateEntities method adjusts the zombie's position relative to the scrolling background.

By separating the logic for walking and scrolling, this system can be easily extended to include additional entities with their own actions and motion relative to the background.


User at 11:17 am, Jun 4, 2024

Ok, now I want to work on the background.

The background is buildings, but they are a bit too tall. They reach to 100% of the height of the canvas, but these are just the foreground buildings. I am going to add background buildings that I want to render in parallax mode and they must be taller than the foreground buildings.

Sow how can I make the foreground buildings shorter without changing the image aspect ratio, or without "sqaushing" the buildings?


Codeium at 11:17 am, Jun 4, 2024

To make the foreground buildings shorter without changing the image aspect ratio or squashing the buildings, you can clip the image vertically when you draw it onto the canvas. This involves drawing only a portion of the image, effectively cropping it to the desired height.

Here's how you can modify your drawBackground method to clip the foreground buildings:

drawBackground(context: CanvasRenderingContext2D) {
  context.globalAlpha = 0.6; // Set to desired transparency level (0 to 1)
  const pattern = context.createPattern(this.bgImage, 'repeat');

  if (pattern) {
    context.fillStyle = pattern;
    context.save(); // Save the current context state

    // Calculate the x-offset for the repeating pattern
    const offsetX = -this.state.backgroundOffsetX % this.bgImage.width;
    context.translate(offsetX, 0);

    // Define the desired height of the buildings on the canvas
    const buildingsHeight = this.props.canvasHeight * 0.8; // For example, 80% of the canvas height

    // Clip the image vertically
    context.rect(0, 0, this.props.canvasWidth, buildingsHeight);
    context.clip();

    // Draw the pattern twice to cover the entire canvas width plus extra space for scrolling
    context.fillRect(offsetX, 0, this.props.canvasWidth - offsetX, buildingsHeight);
    context.fillRect(this.bgImage.width + offsetX, 0, this.props.canvasWidth - offsetX, buildingsHeight);

    context.restore(); // Restore the context state

    // Optionally draw background buildings here using a similar method but with different height/offset
  } else {
    // Handle the null pattern case, perhaps by filling a solid color or logging an error
    context.fillStyle = 'grey'; // Fallback color
    context.fillRect(0, 0, this.props.canvasWidth, this.props.canvasHeight);
  }

  context.globalAlpha = 1; // Reset to full opacity
}

In this example, context.rect creates a rectangle path that defines the clipping region, and context.clip restricts the drawing to within this path. The buildingsHeight variable sets the desired height of the buildings on the canvas. By changing this value, you can adjust how tall the buildings appear without distorting the aspect ratio.

When you add the background buildings, you can apply a different clipping region or offset to render them in parallax mode. Remember that the parallax effect is achieved by moving background layers at a slower speed than the foreground when the scene is scrolled. This creates an illusion of depth.


User at 11:25 am, Jun 4, 2024

That seems to chop the bottom off the buildings. I want the whole buidling, but scaled down at the same rate on both x and y axis.


Codeium at 11:25 am, Jun 4, 2024

To scale down the whole building uniformly without cropping or squashing, you need to adjust the destination width and height while maintaining the aspect ratio when drawing the image onto the canvas. You can calculate the correct scaling factor based on the desired height and then apply it to both the width and height.

Here's how you can modify your drawBackground method to scale down the buildings uniformly:

drawBackground(context: CanvasRenderingContext2D) {
  context.globalAlpha = 0.6; // Set to desired transparency level (0 to 1)

  // Define the desired height of the buildings relative to the canvas height
  const desiredBuildingHeight = this.props.canvasHeight * 0.6; // 60% of the canvas height

  // Calculate the scale factor based on the desired height
  const scale = desiredBuildingHeight / this.bgImage.height;

  // Calculate the scaled width that maintains the aspect ratio
  const scaledWidth = this.bgImage.width * scale;

  const pattern = context.createPattern(this.bgImage, 'repeat');
  if (pattern) {
    context.fillStyle = pattern;
    context.save(); // Save the current context state

    // Calculate the x-offset for the repeating pattern
    const offsetX = -this.state.backgroundOffsetX % scaledWidth;
    context.translate(offsetX, 0);

    // Use the scaled width and desired height to draw the buildings
    context.fillRect(offsetX, this.props.canvasHeight - desiredBuildingHeight, scaledWidth, desiredBuildingHeight);
    context.fillRect(scaledWidth + offsetX, this.props.canvasHeight - desiredBuildingHeight, scaledWidth, desiredBuildingHeight);

    context.restore(); // Restore the context state
  } else {
    // Handle the null pattern case, perhaps by filling a solid color or logging an error
    context.fillStyle = 'grey'; // Fallback color
    context.fillRect(0, 0, this.props.canvasWidth, this.props.canvasHeight);
  }

  context.globalAlpha = 1; // Reset to full opacity
}

In this code, desiredBuildingHeight is the height you want the buildings to be on the canvas. The scale factor is calculated based on the original height of the bgImage and the desiredBuildingHeight. The scaledWidth is then calculated to maintain the aspect ratio. When calling context.fillRect, we use scaledWidth and desiredBuildingHeight to draw the background image at the correct size.

Note that createPattern will not respect the scaling because it repeats the source image as is. If you want to repeat a scaled image, you would have to draw the image multiple times or use an offscreen canvas to create a scaled pattern. Please adjust the logic as necessary to fit your game's needs.


User at 11:39 am, Jun 4, 2024

If I do it like this it chops the bottom and right side of the image off:

 drawBackground(context: CanvasRenderingContext2D) {
  context.globalAlpha = 0.6; // Set to desired transparency level (0 to 1)

  // Define the desired height of the foreground buildings
  const foregroundBuildingHeight = this.props.canvasHeight * 0.6;

  // Calculate the scale factor based on the desired height
  const scale = foregroundBuildingHeight / this.bgImage.height;

  // Calculate the scaled width that maintains the aspect ratio
  const scaledWidth = this.bgImage.width * scale;

  // Assuming you have a backgroundImage and a backgroundOffsetX state
  const pattern = context.createPattern(this.bgImage, 'repeat');

  if (pattern) {
   context.fillStyle = pattern;
   context.save();

   const offsetX = -this.state.backgroundOffsetX % this.bgImage.width;
   context.translate(offsetX, 0);

   // Draw the pattern twice if near the edge to cover the entire canvas plus extra space
   context.fillRect(offsetX, 0, scaledWidth, foregroundBuildingHeight);
   context.fillRect(this.bgImage.width + offsetX, 0, scaledWidth, foregroundBuildingHeight);
   context.restore();
  } else {
   // Handle the null pattern case, perhaps by filling a solid color or logging an error
   context.fillStyle = 'grey'; // Fallback color
   context.fillRect(0, 0, this.props.canvasWidth, this.props.canvasHeight);
  }
  context.globalAlpha = 1; // Set to desired transparency level (0 to 1)

And if I use this:

   // Draw the pattern twice if near the edge to cover the entire canvas plus extra space
   context.fillRect(offsetX, this.props.canvasHeight - foregroundBuildingHeight, scaledWidth, foregroundBuildingHeight);
   context.fillRect(this.bgImage.width + offsetX, this.props.canvasHeight - foregroundBuildingHeight, scaledWidth, foregroundBuildingHeight);
   context.restore();

It chops the top and the right side of the buildings off.


Codeium at 11:39 am, Jun 4, 2024

To scale down the buildings without chopping off any part of the image, you need to change the approach from using a pattern to directly drawing the image with drawImage. The createPattern method won't help here because it repeats the image at its original size and does not allow for scaling.

Here is how you can modify the drawBackground method to draw the scaled buildings:

drawBackground(context: CanvasRenderingContext2D) {
  context.globalAlpha = 0.6; // Set to desired transparency level (0 to 1)

  // Define the desired height of the foreground buildings
  const foregroundBuildingHeight = this.props.canvasHeight * 0.6;

  // Calculate the scale factor based on the desired height
  const scale = foregroundBuildingHeight / this.bgImage.height;

  // Calculate the scaled width that maintains the aspect ratio
  const scaledWidth = this.bgImage.width * scale;

  // Calculate how many times the image should be drawn to cover the canvas width
  const numImages = Math.ceil(this.props.canvasWidth / scaledWidth);

  // Calculate the offset for when the image scrolls
  const offsetX = -this.state.backgroundOffsetX % scaledWidth;

  context.save(); // Save the current context state

  // Draw the scaled image multiple times to cover the canvas width
  for (let i = 0; i < numImages; i++) {
    context.drawImage(
      this.bgImage,
      0, 0, // source X, Y
      this.bgImage.width, this.bgImage.height, // source width and height
      offsetX + (i * scaledWidth), this.props.canvasHeight - foregroundBuildingHeight, // destination X, Y
      scaledWidth, foregroundBuildingHeight // destination width and height
    );
  }

  context.restore(); // Restore the context state
  context.globalAlpha = 1; // Reset to full opacity
}

This code uses context.drawImage to draw the background image multiple times across the width of the canvas. Each image is scaled uniformly to maintain the aspect ratio. The offsetX variable is used to ensure that the images scroll with the background. The loop uses numImages to determine how many times the image needs to be drawn to cover the entire canvas width.

By using drawImage, you avoid the issues that createPattern presented, and you can scale the image as needed without cropping or squashing.


User at 11:44 am, Jun 4, 2024

That looks better, but it reverts back to leaving a gap until a new image is drawn.

I need the incoming scroll images to already exist so that there is no gap on the right side of the screen.


Codeium at 11:45 am, Jun 4, 2024

To prevent a gap on the right side of the canvas when scrolling, you should ensure that an additional copy of the image is drawn just off-screen to the right. This way, as the background scrolls left, there's already an image ready to come into view.

Here's how you can adjust the drawBackground method to always draw an extra image to the right:

drawBackground(context: CanvasRenderingContext2D) {
  context.globalAlpha = 0.6; // Set to desired transparency level (0 to 1)

  // Define the desired height of the foreground buildings
  const foregroundBuildingHeight = this.props.canvasHeight * 0.6;

  // Calculate the scale factor based on the desired height
  const scale = foregroundBuildingHeight / this.bgImage.height;

  // Calculate the scaled width that maintains the aspect ratio
  const scaledWidth = this.bgImage.width * scale;

  // Calculate how many times the image should be drawn to cover the canvas width
  // Add 1 to the numImages to draw an extra image off-screen
  const numImages = Math.ceil(this.props.canvasWidth / scaledWidth) + 1;

  // Calculate the offset for when the image scrolls
  const offsetX = -this.state.backgroundOffsetX % scaledWidth;

  context.save(); // Save the current context state

  // Draw the scaled image multiple times to cover the canvas width and the extra off-screen image
  for (let i = 0; i < numImages; i++) {
    context.drawImage(
      this.bgImage,
      0, 0, // source X, Y
      this.bgImage.width, this.bgImage.height, // source width and height
      offsetX + (i * scaledWidth), this.props.canvasHeight - foregroundBuildingHeight, // destination X, Y
      scaledWidth, foregroundBuildingHeight // destination width and height
    );
  }

  context.restore(); // Restore the context state
  context.globalAlpha = 1; // Reset to full opacity
}

By adding 1 to numImages, you're ensuring that there's always an extra image to the right. When offsetX is negative, it means that the image is being scrolled left and part of the first image is off-screen to the left. At this point, the extra image on the right comes into view, filling the gap.

This method should give you a seamless scrolling background without gaps. Adjust the number of extra images if necessary, depending on the specifics of your game's canvas size and background image size.


User at 11:46 am, Jun 4, 2024

That works.


Codeium at 11:47 am, Jun 4, 2024

Great to hear that it works! If you have any more questions or need further assistance with your game, feel free to ask. Happy coding!


User at 11:51 am, Jun 4, 2024

Now I need to add a second set of building images. These should load behind the foreground images and should be at 80% scale.

The file is at public\images\parallax-industrial-pack\parallax-industrial-pack\layers\skill-desc_0001_buildings.png

They should move at 60% of the rate of the bgImage.


Codeium at 11:52 am, Jun 4, 2024

To add a second set of building images behind the foreground and have them scale at 80% and move at 60% of the rate of the bgImage, you will need to follow these steps:

  1. Load the second set of buildings as a new image in your component.
  2. Draw the background buildings at the desired scale.
  3. Move the background buildings at a slower rate to achieve the parallax effect.

Here's how you can incorporate these steps into your TerminalGame component:

First, load the new background buildings image in the TerminalGame component:

class TerminalGame extends React.Component {
  // ... other fields
  private bgBuildingsImage = new Image();

  componentDidMount() {
    // ... existing code
    this.bgBuildingsImage.src = '/images/parallax-industrial-pack/parallax-industrial-pack/layers/skill-desc_0001_buildings.png';
    // Optionally, handle onload and onerror for this.bgBuildingsImage
  }

  // ... other methods
}

Then, you can adjust the drawBackground method to include the drawing of the background buildings:

drawBackground(context: CanvasRenderingContext2D) {
  // ... existing code to draw the foreground buildings

  // Define the scale for the background buildings
  const bgBuildingsScale = 0.8; // 80% scale
  const bgBuildingsHeight = this.props.canvasHeight * bgBuildingsScale;
  const bgBuildingsScaledWidth = this.bgBuildingsImage.width * bgBuildingsScale;

  // Calculate how many times the background image should be drawn to cover the canvas width
  const bgNumImages = Math.ceil(this.props.canvasWidth / bgBuildingsScaledWidth) + 1;

  // Calculate the offset for when the background image scrolls (60% of the foreground rate)
  const bgOffsetX = -(this.state.backgroundOffsetX * 0.6) % bgBuildingsScaledWidth;

  // Draw the background buildings
  context.save(); // Save the current context state
  for (let i = 0; i < bgNumImages; i++) {
    context.drawImage(
      this.bgBuildingsImage,
      0, 0, // source X, Y
      this.bgBuildingsImage.width, this.bgBuildingsImage.height, // source width and height
      bgOffsetX + (i * bgBuildingsScaledWidth), this.props.canvasHeight - bgBuildingsHeight, // destination X, Y
      bgBuildingsScaledWidth, bgBuildingsHeight // destination width and height
    );
  }
  context.restore(); // Restore the context state

  // ... existing code to reset globalAlpha if needed
}

In this modified drawBackground method:

  • bgBuildingsScale defines the scale factor for the background buildings.
  • bgBuildingsHeight is the scaled height of the background buildings.
  • bgBuildingsScaledWidth is the scaled width of the background buildings.
  • bgNumImages is the number of times to draw the background image to ensure it covers the canvas width.
  • bgOffsetX is the horizontal offset for the background buildings, which moves at 60% of the bgImage scroll speed.

Make sure to call drawBackground within your animation loop to continuously update the positions and render both the foreground and background buildings with the parallax effect.

Please adjust any hardcoded values like scales and rates to fit your game's design and aesthetic.


User at 12:01 pm, Jun 4, 2024

That sort of looks like you dropped the original buildings images. I need both of them so they can move in parallax.


Codeium at 12:01 pm, Jun 4, 2024

Apologies for the confusion. To draw both sets of building images with a parallax effect, you'll need to draw the foreground buildings as you already have, and then draw the second set of background buildings at a different scale and movement rate.

Here's how to adjust the drawBackground method to include both the foreground and background buildings:

drawBackground(context: CanvasRenderingContext2D) {
  // Draw background buildings first, so they appear behind the foreground buildings
  this.drawParallaxLayer(
    context,
    this.backgroundBuildings, // the image for the background layer
    0.8, // scale for the background buildings
    0.6 // rate of movement relative to the foreground
  );

  // Draw foreground buildings on top of the background buildings
  this.drawParallaxLayer(
    context,
    this.foregroundBuildings, // the image for the foreground layer
    0.6, // scale for the foreground buildings
    1 // rate of movement (1 to move at the same rate as the scrolling offset)
  );
}

drawParallaxLayer(context: CanvasRenderingContext2D, image: HTMLImageElement, scale: number, movementRate: number) {
  const layerHeight = this.props.canvasHeight * scale;
  const scaledWidth = image.width * scale;
  
  // Calculate how many times the image should be drawn to cover the canvas width
  const numImages = Math.ceil(this.props.canvasWidth / scaledWidth) + 1;

  // Calculate the offset for when the image scrolls
  const offsetX = -(this.state.backgroundOffsetX * movementRate) % scaledWidth;

  context.save(); // Save the current context state
  context.globalAlpha = scale === 0.8 ? 0.5 : 0.6; // Adjust transparency for effect if desired

  // Draw the scaled image multiple times to cover the canvas width
  for (let i = 0; i < numImages; i++) {
    context.drawImage(
      image,
      0, 0, // source X, Y
      image.width, image.height, // source width and height
      offsetX + (i * scaledWidth), this.props.canvasHeight - layerHeight, // destination X, Y
      scaledWidth, layerHeight // destination width and height
    );
  }

  context.restore(); // Restore the context state
}

In this code:

  • drawParallaxLayer is a new method that handles drawing an individual layer of buildings. It takes the context, the image, the desired scale, and the movement rate as parameters.
  • The drawParallaxLayer method calculates the number of images needed to cover the canvas width and the offset based on the movement rate.
  • The drawBackground method now calls drawParallaxLayer twice, once for each building layer, specifying the appropriate scale and movement rate for each.

This setup will render the background buildings at 80% scale, moving at 60% of the foreground movement rate, creating a parallax scrolling effect. The foreground buildings are drawn at 60% scale and move with the full scrolling offset. Make sure to adjust the transparency values and other parameters to fit the desired visual effect.

⚠️ **GitHub.com Fallback** ⚠️