Source: PlayScene-classes/CloudManager.js

import FlappyState from "./FlappyState";

const States = {
	IDLE: 0,
	RUNNING: 1,
	PAUSED: 2
}

/**
 * @classdesc - This class is used to manage, update, and setup cloud sprites that animated at different speeds and depths to simulate 3D space.
 * This class implements a custom algorithm that I made to draw clouds that are evenly spaced but also with randomness
 * added on to give a more organic look and feel.
 * @author Christian P. Auman
 * @class
 * @memberOf module:PlayScene
 */
class CloudManager extends FlappyState {
	/**
	 * Setting the default values for the class variables
	 * @param scene - the Phaser.Scene object used for adding images/sprites/physics/etc...
	 */
	constructor(scene) {
		super(States.IDLE)
		this.scene = scene
		this.horizontalSections = 10
		this.verticalSections = 3
		this.startingVelocityX = -15
		this.cloudImageSize = {
			width: 200,
			height: 200,
		}
		this.clouds = []
		this.alphas = [0.7, 0.5, 0.3]
		this.verticalOffset = 0
	}
	
	/**
	 * This function is called before anything is drawn on the canvas.
	 * This allows for asset preloading which prevents anything from drawing without
	 * it first loading the asset.
	 */
	preload() {
		this.scene.load.image('cloud', '/assets/cloud-1.png')
	}
	
	/**
	 * This method is the core feature to drawing the actual clouds. Originally, this method created many
	 * cloud game-objects with each one having their own physics body. However, this way of creating
	 * the clouds I found to be more cpu intensive than I wanted. So I came up with my own way to make it less
	 * of a hassle for the player's computer. I was looking through the Phaser docs and came across a way to actually
	 * create a custom texture dynamically. Then I ended up applying this to my algorithm and in the end, I made it so
	 * the game only has to update 6 different images compared to the 50 I had before. This method of creation can be scaled
	 * infinitely and will have the exact same memory usage if you were to have 6 clouds.
	 */
	setupClouds() {
		// reusable variables for abstraction and re-usability
		const canvasHeight = this.scene.game.canvas.height - this.verticalOffset
		const sectionHeight = canvasHeight / this.verticalSections
		
		// configuration for the final texture size
		const textureConfig = {
			width: this.scene.game.canvas.width,
			height: this.scene.game.canvas.height - this.verticalOffset + this.cloudImageSize.height
		}
		// creating a renderTexture to draw the clouds on
		this.background = this.scene.make.renderTexture(textureConfig)
		this.middleground = this.scene.make.renderTexture(textureConfig)
		this.foreground = this.scene.make.renderTexture(textureConfig)
		
		// start drawing the clouds
		this.background.beginDraw()
		this.middleground.beginDraw()
		this.foreground.beginDraw()
		/**
		 * For however many vertical sections given, it will draw a row at each section.
		 */
		for (let i = 0; i < this.verticalSections; i++) {
			this.drawRow(sectionHeight * i, sectionHeight + sectionHeight * i, this.foreground, this.middleground, this.background)
		}
		// I came across a small bug where the last cloud drawn was being placed on each texture renderer and
		// drawing this empty string fixed the issue
		this.background.draw('')
		this.foreground.draw('')
		this.middleground.draw('')
		// end of drawing
		this.background.endDraw()
		this.foreground.endDraw()
		this.middleground.endDraw()
		
		// deleting whatever was inside the clouds array previously
		this.clouds.length = 0
		
		// saving the texture to the Game's TextureManager which allows for dynamically accessing this new
		// texture from just using a given key - string
		this.background.saveTexture('bgClouds')
		this.middleground.saveTexture('mgClouds')
		this.foreground.saveTexture('fgClouds')
		
		// destroys and removes the render textures from the scene which will be picked up by the garbage collectors
		this.foreground.destroy()
		this.middleground.destroy()
		this.background.destroy()
		this.foreground = null
		this.middleground = null
		this.background = null
		
		/**
		 * Here I am creating images using the new textures I just dynamically made, and giving them each a physics body.
		 * It creates 3 different images. One being the foreground clouds which are the biggest and most visible ones. The next
		 * is the middle ground which is in between the foreground and the background and has a less opacity than the foreground clouds.
		 * The last is the background which is drawn behind the other two images.
		 *
		 * The reason there are 2 iteration is that once a cloud group image has moved a little to the left, there is nothing coming
		 * after it, so it will just be blank space. It creates 2 groups of 3 different images, being the foreground/middleground/background,
		 * and the first group is what the user first sees and the second group is positioned 100% off the canvas to the right. This way it adds
		 * a simulated infinite number of clouds when they get recycled.
		 */
		for (let i = 0; i < 2; i++) {
			this.clouds.push(this.scene.physics.add.image(0, 0, 'fgClouds')
				.setOrigin(0, 0)
				.setPosition((this.scene.game.canvas.width)*i - this.cloudImageSize.width / 2, 0)
				.setDepth(-11)
				.setVelocity(this.startingVelocityX - 5, 0)
			)
			this.clouds.push(this.scene.physics.add.image(0, 0, 'mgClouds')
				.setOrigin(0, 0)
				.setPosition((this.scene.game.canvas.width)*i - this.cloudImageSize.width / 2, 0)
				.setDepth(-12)
				.setVelocity(this.startingVelocityX - 0, 0)
			)
			this.clouds.push(this.scene.physics.add.image(0, 0, 'bgClouds')
				.setOrigin(0, 0)
				.setPosition((this.scene.game.canvas.width)*i - this.cloudImageSize.width / 2, 0)
				.setDepth(-13)
				.setVelocity(this.startingVelocityX + 5, 0)
			)
		}
	}
	
	/**
	 * This method is the core piece to the algorithm for creating the clouds. This method is used to create, position, and draw the clouds to
	 * the renderTextures. I basically implemented my own grid and randomly placed each cloud inside that grid cell. It will randomly pick which
	 * renderTexture it draws to using a weighted random pick out of an array.
	 * @param minY - the minimum y position the cloud will draw to
	 * @param maxY - the maximum y position the cloud will draw to
	 */
	drawRow(minY, maxY) {
		// reusable variables for abstraction and re-usability
		const canvasWidth = this.scene.game.canvas.width
		const sectionWidth = canvasWidth / this.horizontalSections
		
		// for however many horizontal sections given, it will draw a cloud inside each section.
		for (let j = 0; j < this.horizontalSections; j++) {
			
			// creating a new cloud game-object
			const cloud = this.scene.add.image(0, 0, 'cloud')
			
			// setting up the minimum and maximum x values the cloud could be positioned at
			let minX = sectionWidth * j
			let maxX = sectionWidth + sectionWidth*j
			// if it's the first iteration, it means that the cloud has a chance to be positioned
			// outside the boundaries which would create a cloud that is cutt-off. Therefore, if
			// it is the first iteration, it will alter the minimum x value accordingly
			if (j === 0) {
				minX += this.cloudImageSize.width / 2
			}
			// this if statement is the same as the one above except it is checking to see if it's the
			// last iteration
			if (j === this.horizontalSections - 1) {
				maxX -= this.cloudImageSize.width / 2
			}
			
			// will randomly choose an element from the alphas array with more of a chance to pick the first element than the last one.
			// This way, it allows for more close up clouds than far away clouds or even reverse.
			const alpha = Phaser.Math.RND.weightedPick(this.alphas)
			
			//setting the opacity/alpha of the cloud
			cloud.alpha = alpha
			
			/**
			 * Depending on which alpha the cloud got, it will draw it to its subsequent textureRenderer and will set the scale of the
			 * cloud accordingly
			 */
			if (alpha === this.alphas[2]) {
				cloud.scale = .3
				this.background.draw(cloud, Phaser.Math.RND.between(minX, maxX), Phaser.Math.RND.between(minY, maxY))
			} else if (alpha === this.alphas[1]) {
				cloud.scale = .45
				this.middleground.draw(cloud, Phaser.Math.RND.between(minX, maxX), Phaser.Math.RND.between(minY, maxY))
			} else if (alpha === this.alphas[0]){
				cloud.scale = .6
				this.foreground.draw(cloud, Phaser.Math.RND.between(minX, maxX), Phaser.Math.RND.between(minY, maxY))
			}
			cloud.destroy()
		}
	}
	
	/**
	 * sets up the clouds initially
	 */
	create() {
		this.setupClouds()
	}
	
	/**
	 * This is the core part to the pooling/recycling of the clouds. Once one of the cloud grouped images
	 * is 100% past the left edge of the canvas, it will reposition 100% past the far right edge which gives an illusion
	 * of infinite clouds.
	 */
	update() {
		this.clouds.map(cloudGroup => {
			if (cloudGroup.x <= -cloudGroup.displayWidth) {
				cloudGroup.x = this.scene.game.canvas.width
			}
		})
	}
	
	/**
	 * called at the start of the game. This is for if this class ever needs to execute code at the start of the game
	 */
	startG() {
		super.start()
	}
	
	/**
	 * called at the end of the game or death. This will set the state to PAUSED and then maps through
	 * each cloud group and sets the velocity to -5 giving a much slower movement since the player will no
	 * longer be moving forward because they have died
	 */
	stopG() {
		super.stop()
		this.clouds.map(cloud => {
			cloud.body.velocity.x = -5
		})
	}
	
	/**
	 * called whenever the game state changes from dead/paused to idle. This resets everything including the positions of the clouds
	 * and creates new textures to use giving more variability to the game.
	 */
	resetG() {
		super.reset()
		this.clouds.map(cloud => {
			cloud.destroy(true)
		})
		this.clouds.length = 0
		this.setupClouds()
	}
}

export default CloudManager