In the following code, an outer div
forms the boundaries of a playfield and all game elements inside the playfield are represented by div
s.
Game state consists of an array of game elements (in this instance: one ship and zero or more bullets). Once an element is no longer visible (here: simply off the right-hand side of the playfield), it is removed from game state.
The game loop uses requestAnimationFrame
to repeatedly render the game state to the playfield.
Bullet position is calculated using the time of firing and the time elapsed (I added a little randomness to bullet velocity just for fun).
Game elements such as bullets have an associated generator function called as part of the game loop, to retrieve the next state of the element (a bullet "moves by itself" after the initial appearance).
Firing a bullet in this design is as simple as creating a new bullet object with an initial position and an instance of a generator function to account for its trajectory; and then adding that pair to the game state.
const elem = ({ kind = 'div', classN = '' }) => { const el = document.createElement(kind) el.classList.add(classN) return el}const applyStyle = (el, style) => (Object.entries(style) .forEach(([k, v]) => el.style[k] = v), el)const cssPixels = (str) => +(str.slice(0, -2))const isVisible = (left) => cssPixels(left) < cssPixels(playfield.style.width)const createPlayfield = () => applyStyle(elem({ classN: 'playfield' }), { width: '300px' })const createShip = (startLeft, width) => [{ classN: 'ship', style: { left: startLeft, width } }, null]const createBullet = (startLeft) => { const b = { classN: 'bullet', style: { left: startLeft }, firingTime: +new Date(), velocity: 0.5, velocitySeed: Number('1.'+ ~~(Math.random() * 9)), startLeft } const g = bulletStateGen(b) return [ b, () => g.next() ]} const bulletPos = ({ firingTime, startLeft, velocity, velocitySeed }, now = +new Date()) => `${~~(velocity * (now - firingTime) * velocitySeed + cssPixels(startLeft))}px`const bulletStateGen = function*(b) { while (1) { const left = bulletPos(b) if (!isVisible(left)) break b.style = { left } yield(b) }}const fire = (startLeft) => state.unshift(createBullet(startLeft))const tick = () => state = state.reduce((acc, [o, next]) => { if (!next) return acc.push([o, next]), acc const { value, done } = next() if (done) return acc return acc.push([value, next]), acc }, [])const blank = () => playfield.innerHTML = ''const render = () => { blank() state.forEach(([{ classN, style = {} }]) => playfield.appendChild(applyStyle(elem({ classN }), style)))}let ship = createShip('10px', '50px')let state = [ship]let playfield = createPlayfield()const gameLoop = () => (render(), tick(), requestAnimationFrame(gameLoop))const init = () => { document.body.appendChild(playfield) document.body.onkeyup = (e) => e.key === ""&& fire(`${cssPixels(ship[0].style.left) + cssPixels(ship[0].style.width)}px`)}init()gameLoop(state, playfield)
.playfield { height: 300px; background-color: black; position: relative;}.ship { top: 138px; height: 50px; background-color: gold; position: absolute; border-radius: 7px 22px 22px 7px;}.bullet { top: 163px; width: 10px; height: 2px; background-color: silver; position: absolute;}
Click on the game to focus it, and then press spacebar to fire!