I am new to game development and took on the task to create a Pong replica due to my curiosity and interest in the field. I decided to write it in JavaScript because the game is not too labor intensive. I specially wrote it without any libraries for learning purposes.
One of the biggest issues that I seem to run into is making an AI that is good, but not too good. I can make one that is perfect, and I can also make one that is absolutely terrible but I can not seem to find any in between with my code.
The purpose of this code review is that I hope someone can suggest how I can fix my AI issue and to provide me with information on how to more efficiently code this game.
Hit the "(Re)start" button to demo the code.
// get a random signed number
window.signedRand = function (min, max, unrounded){
let sign = Math.random() > 0.5 ? -1 : 1;
let randRange = Math.random() * (max - min + 1);
let minRange = (unrounded ? randRange : Math.floor(randRange))
return sign * (minRange + min);
}
Math.addIgnoreSign = function (a, b) {
return Math.sign(a) * Math.abs(a + b);
}
let Pong = function () {
let canvas = document.getElementById("canvas");
let WIDTH = canvas.width;
let HEIGHT = canvas.height;
let acceleration = 0.5; // sets the acceleration everytime the ball its a board
let playing = true; // true if the game is running
let AIHelper = true; // if this is true then we are going to help the AI out by speeding it up
let ghostReaction = 6; // a higher number == slower reaction to the ghost ball
// true reaction is half on the first hit (so it doubles after first hit)
let trueReaction = 4; // a heigher number == slower reaction to the true/real ball
let ghostSpeed = 10; // the max speed of the ghost ball
let paddleSpeed = 7; // the speed of the paddle
var ctx = canvas.getContext("2d");
ctx.fillStyle = ctx.strokeStyle = "white";
ctx.font = '48px serif';
let Elements = {}; // hold the different drawing elements
let Player1Score = 0;
let Player2Score = 0;
function calculateChangeInY(ballY, paddle) {
let totalPaddle = paddle.height;
let center = paddle.y + totalPaddle/2;
let distanceFromCenter = ballY - center
return 12 * Math.sin(distanceFromCenter/totalPaddle * (Math.PI/2));
}
let KeyBinding = new (function () { // keep track of keys that are pressed
let pressing = new Set(); // ensure uniqueness
let keyMap = { // map keys for convenience and readable while in use
"uparrow": 38,
"downarrow": 40,
"w": 87,
"s": 83
}
this.register = function (keyCode) { // add a key
pressing.add(keyCode);
}
this.clear = function (keyCode) { // remove a key
pressing.delete(keyCode);
}
this.pressing = function (keyname) { // check if the key is pressed by checking if it is availiable
return pressing.has(keyMap[keyname]);
}
// Register Key Bindings
window.addEventListener('keydown', function (event) { KeyBinding.register(event.keyCode); }, false);
window.addEventListener('keyup', function (event) { KeyBinding.clear(event.keyCode); }, false);
})();
// not really important to the functionality, but will add an effect for when the ball hits the paddle
let Emitter = new (function (number) {
let Particles = null;
let Particle = function (particleID, xco, yco) {
let ang = particleID/number*2*Math.PI; // get the desired angle that you want the particle to move
let x = xco; // starting x
let y = yco; // starting y
let xi = Math.cos(ang); // change in x
let yi = Math.sin(ang); // change in y
let radius = 1;
this.draw = function () {
if (radius > 0) {
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
x += xi;
y += yi;
radius -= 0.05; // make the particles fade away
} else {
delete Particles[particleID];
}
}
}
this.set = function (xco, yco) {
Particles = {};
for (let i = 0; i < number; i++) {
Particles[i] = new Particle(i, xco, yco);
}
}
this.draw = function () {
for (let particle in Particles) {
Particles[particle].draw();
}
}
// 15 number of particles
})(15);
let Ball = function (x_coord, y_coord, radius, hidden) {
this.xi = signedRand(7,9); //every ball starts with a unique velocity
this.yi = signedRand(1, 5);
this.x = x_coord;
this.y = y_coord;
this.radius = radius;
this.hitTopBoundary = function () { // checks if the ball its the top or bottom
return this.y + this.yi > HEIGHT - radius || this.y + this.yi < radius;
}
this.outOfBound = function () { // checks if the ball is out of bound
return this.x > WIDTH + radius || this.x < -radius;
}
this.draw = function () {
if (!hidden) {
ctx.beginPath(); // draw ball
ctx.arc(this.x, this.y, radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
if (this.outOfBound()) {
if (this.x > WIDTH/2) { // if on right side
Player2Score++;
} else {
Player1Score++;
}
playing = false;
// reset the game
setTimeout(() => {
Elements.ball = new Ball(WIDTH/2, HEIGHT/2, 7);
if (!AIHelper) trueReaction /= 2;
acceleration = 0.5;
paddleSpeed = 6;
playing = true;
AIHelper = true;
}, 1500);
}
}
if (this.outOfBound()) Elements.ghostBall = null;
if (this.hitTopBoundary()) {
this.yi = -this.yi;
}
// update positioning
this.x += this.xi;
this.y += this.yi;
}
}
let Paddle = function (x) {
this.height = 100;
this.width = 10;
this.x = x;
this.y = HEIGHT/2 - this.height/2; // initial position should be in the center
let movement = function () {}; // handles movement this.init
this.hitLeftPaddle = function (b1) { // detection from the left side of the paddle
if (b1 == null) return false;
let rightSide = this.width + this.x; // off by 30 and width of 10
let top = this.y;
let bottom = top + this.height;
// We are basically trying to see if the ball has touched the box
return b1.x + b1.xi < rightSide + b1.radius &&
b1.x - b1.xi > this.x &&
b1.y > top - b1.radius &&
b1.y < bottom + b1.radius
}
this.hitRightPaddle = function (b1) { // detection from the right side of the paddle
if (b1 == null) return false;
let leftSide = this.x; // off by 30 and width of 10
let top = this.y;
let bottom = top + this.height;
// We are basically trying to see if the ball has touched the box
return b1.x + b1.xi > leftSide - b1.radius &&
b1.x - b1.xi < this.x + this.width &&
b1.y > top - b1.radius &&
b1.y < bottom + b1.radius
}
this.bindMovement = function (movementStatement) { // bind the movement function
movement = movementStatement;
}
this.draw = function () {
ctx.beginPath(); // draw the paddle
ctx.rect(this.x, this.y, this.width, this.height);
ctx.fill();
ctx.closePath();
////
// We move the paddle based on function based in so that we have flexiblity
// to add a computer or switch to two players.
////
let move = movement.call(this);
if (move != null) this.y = this.y + move;
if (this.y < 0) this.y = 0;
if (this.y > (HEIGHT - this.height)) this.y = HEIGHT - this.height;
}
}
let generateGhost = function () { // creates a ghost based on the
Elements.ghostBall = new Ball(Elements.ball.x, Elements.ball.y, 7, true);
Elements.ghostBall.xi = Math.addIgnoreSign(Elements.ball.xi, ghostSpeed * Math.random());
Elements.ghostBall.yi = Elements.ball.yi + Math.random()/7;
}
this.init = function () {
////
// Create our three moving parts to the game
////
Elements.ball = new Ball(WIDTH/2, HEIGHT/2, 7);
Elements.paddle1 = new Paddle(30);
Elements.paddle2 = new Paddle(WIDTH - 40);
Elements.paddle1.bindMovement(function () { // binds the movement of paddle 1 to the up and down arrows
if (KeyBinding.pressing("uparrow")) return -paddleSpeed;
if (KeyBinding.pressing("downarrow")) return paddleSpeed;
});
// for now we are using a bot with the help of a ghost ball
// the ghost ball appears everytime the ball its the left paddle and goes away when it hits the right
Elements.paddle2.bindMovement(function () {
let padY = this.y + this.height/2;
if (Elements.ghostBall != null) {
return (Elements.ghostBall.y - padY)/ghostReaction;
} else {
return (Elements.ball.y - padY)/trueReaction;
}
});
/*function () { // optionally to make this two players, bind paddle 2 to w and s
if (KeyBinding.pressing("w")) return -paddleSpeed;
if (KeyBinding.pressing("s")) return paddleSpeed;
}*/
}
this.start = function () {
this.init();
this.draw();
};
this.stop = () => playing=false;
this.draw = function () {
ctx.clearRect(0, 0, WIDTH, HEIGHT);
Emitter.draw();
////
// Redraw all elements
////
Elements.paddle1.draw(); // paddles can move even if the rest of the game does not
Elements.paddle2.draw();
if (playing) { // if we are playing
Elements.ball.draw();
// if we have a ghost ball then we should draw it
if (Elements.ghostBall) Elements.ghostBall.draw();
// save us some work while the ball is traveling since it can not possible
// hit the paddle if it isnt below or above these ranges
if (Elements.ball.x < 50 || Elements.ball.x > WIDTH-50) {
if (Elements.paddle1.hitLeftPaddle(Elements.ball)) {
////
// calculateChangeInY will change the change in y depending on where on the paddle we hit
////
Elements.ball.yi = calculateChangeInY(Elements.ball.y, Elements.paddle1); // change vertical velocity of the ball
Elements.ball.xi = -Math.addIgnoreSign(Elements.ball.xi, acceleration); // switch the x direction with acceleration
generateGhost(); // generate our ghost
Emitter.set(Elements.ball.x, Elements.ball.y); // emit the particle effect
paddleSpeed += acceleration/1.5; // speed up the paddle a little less than the ball is speeding up
} else {
if (Elements.paddle2.hitRightPaddle(Elements.ball)) {
if (AIHelper) {
trueReaction *= 2;
AIHelper = false;
}
Elements.ball.yi = calculateChangeInY(Elements.ball.y, Elements.paddle2); // change vertical velocity of the ball
Elements.ball.xi = -Math.addIgnoreSign(Elements.ball.xi, acceleration); // switch the x direction with acceleration
Emitter.set(Elements.ball.x, Elements.ball.y); //emit the particle effect
}
}
}
if (Elements.ghostBall && Elements.ghostBall.x > WIDTH-50) {
if (Elements.paddle2.hitRightPaddle(Elements.ghostBall)) {
Elements.ghostBall = null;
}
}
}
// Some design while the game is ongoing
ctx.fillText(Player1Score, WIDTH/2 - 100, 50);
ctx.fillText(Player2Score, WIDTH/2 + 75, 50);
ctx.setLineDash([10, 10]);
ctx.moveTo(WIDTH/2, 0);
ctx.lineTo(WIDTH/2, HEIGHT);
ctx.stroke();
window.requestAnimationFrame(this.draw.bind(this));
}
};
var PongGame;
document.getElementById("restart").addEventListener('click', function () {
if (PongGame != null) PongGame.stop();
PongGame = new Pong();
PongGame.start();
});
canvas{
background: black;
}
<button id="restart">
(Re)start
</button>
<canvas width="500" height="400" id="canvas" style="border: 1px solid"></canvas>
-
1\$\begingroup\$ Welcome to Code Review! We'll be more than happy to review your code for improvements, however we can't really comment on your specific concern, AI difficulty. This is more of a game design concern, and will also vary depending on who is playing against your AI. For questions like these, feel free to check out Game Development. There is a difficulty tag on there specifically for questions like that. You may need to boil down your concerns to plain English though, not sure they would read all this code to understand it. \$\endgroup\$Phrancis– Phrancis2018年04月12日 04:46:56 +00:00Commented Apr 12, 2018 at 4:46
1 Answer 1
As Phrancis alluded to in the comment, this review pertains to the existing code and has no remarks about the AI.
Feedback
The game appears to function well, and the code appears to be organized somewhat well. The CSS and HTML are very simple but sufficient.
Suggestions
Instance/Prototype Methods: It would be much more efficient to define the methods on the prototypes instead of attaching to each instance. That way each method is declared only once instead of once per each instance. That would have huge impacts on the memory used by the browser. For more information about that topic, read about it in this post.
The code would need to be re-worked to use either more instance variables or variables with scope larger than the current functions/closures.
Because ecmascript-6 features like arrow functions and the
let
keyword are used, the class notation could be used also.- Indentation: some lines are indented with four spaces while others are indented with tab characters. It is simple to see when pasted into the code blocks here- e.g. the first line of the
Pong()
constructor looks like it is indented with 8 spaces but upon further inspection it appears that it is a tab character. const
vslet
: - it appears thatlet
is used exclusively. It is wise to useconst
for any value that shouldn't get re-assigned - especially constant values. This can help avoid unintentional re-assignment.- CONSTANT NAMES: Most style guides recommend constant values have all capital letters in the name - e.g.
ghostspeed
would be changed toGHOST_SPEED
. The code already hasWIDTH
andHEIGHT
but those almost seem excessive - the code could just usecanvas.width
andcanvas.height
in their place. - variable names: most JS style guides recommend using
camelCase
for variable names andProperCase
for class names/constructor methods. for...of
loops: could be used to simplify looping - e.g.for (let particle in Particles) { Particles[particle].draw(); }
could be simplified to:
for (const particle of Particles) { particle.draw(); }
Explore related questions
See similar questions with these tags.