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:
-
Background Repeating: A solution was provided to ensure the background image repeats smoothly by using the
createPattern()
method in thedrawBackground
function. -
Character Movement: Adjusting the hero's movement relative to the canvas and stopping it when the background scrolls involved passing an
inScrollMode
prop fromCharacterActionComponent
toBaseCharacter
. -
Preloading Background: To avoid gaps when scrolling, preloading background segments beyond the visible canvas area was suggested by modifying the
drawBackground
method. -
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.
-
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
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:
-
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 ofsetState
that provides the previous state as an argument, or manage the state in a more synchronous manner. -
Heavy Rendering: Ensure that the
drawBackground
and character drawing functions are optimized and not causing the main thread to be blocked. -
Throttling
setState
Calls: IfsetState
is being called too frequently, it can cause performance issues. Throttle the calls tosetState
for the background and character positions if needed. -
Animation Loop: Make sure that the game's animation loop (
requestAnimationFrame
) is functioning correctly and that theupdateCharacterAndBackground
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 multiplesetState
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:
-
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. -
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.
-
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:
- Load the second set of buildings as a new image in your component.
- Draw the background buildings at the desired scale.
- 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 thebgImage
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 callsdrawParallaxLayer
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.