I'm writing a version of Pong in Javascript. I have a Game object and I'm using the prototype property to define methods on it. I'm getting the following error: "Undefined is not a function". It is being thrown in the Game.prototype.step function so this.update is undefined in there. Here's the code for the Game object:
(function(root) {
var Pong = root.Pong = (root.Pong || {});
var Game = Pong.Game = function() {
this.canvas = document.getElementById('canvas');
this.canvas.width = 800;
this.canvas.height = 400;
this.context = canvas.getContext('2d');
this.maxStartSpeed = 10;
this.keysDown = {};
this.player2 = new Pong.Player({'player': 2});
this.player1 = new Pong.Player({'player': 1});
this.ball = new Pong.Ball(400, 200);
}
Game.prototype.update = function() {
this.player1.update();
this.player2.update();
this.ball.update(player1.paddle, player2.paddle);
};
Game.prototype.render = function() {
this.context.fillStyle = "#bdc3c7";
this.context.fillRect(0, 0, width, height);
this.player1.render();
this.player2.render();
this.ball.render();
};
Game.prototype.animate = function(callback) {
window.setTimeout(callback, 1000/60)
};
Game.prototype.step = function() {
this.update();
this.animate(this.step);
};
window.addEventListener("keydown", function (event) {
Game.keysDown[event.keyCode] = true;
});
window.addEventListener("keyup", function (event) {
delete Game.keysDown[event.keyCode];
});
window.onload = function() {
document.getElementById('canvas-container').appendChild(canvas);
game = new Game();
game.animate(game.step);
};
})(this);
The setTimeout is going to change the scope. To maintain the proper scope, you need to use bind
Change
this.animate(this.step);
to
this.animate(this.step.bind(this));
You need to do the same thing with the other animate calls.
Related
I am making a javascript game, using Canvas. It works well superficially, but "Uncaught TypeError: game_state.Update is not a function" keeps going. I cannot know the reason for all day...How can I solve the problem?
error image1, error image2
Suspected files are below.
gfw.js
function onGameInit()
{
document.title = "Lion Travel";
GAME_FPS = 30;
debugSystem.debugMode = true;
//resourcePreLoading
resourcePreLoader.AddImage("/.c9/img/title_background.png");
resourcePreLoader.AddImage("/.c9/img/title_start_off.png");
resourcePreLoader.AddImage("/.c9/img/title_start_on.png");
resourcePreLoader.AddImage("/.c9/img/title_ranking_off.png");
resourcePreLoader.AddImage("/.c9/img/title_ranking_on.png");
resourcePreLoader.AddImage("/.c9/img/game_background_sky.png");
soundSystem.AddSound("/.c9/background.mp3", 1);
after_loading_state = new TitleState();
game_state = TitleState;
setInterval(gameLoop, 1000 / GAME_FPS);
}
window.addEventListener("load", onGameInit, false);
GameFramework.js
window.addEventListener("mousedown", onMouseDown, false);
window.addEventListener("mouseup", onMouseUp, false);
var GAME_FPS;
var game_state;
function onMouseDown(e)
{
if(game_state.onMouseDown != undefined)
game_state.onMouseDown(e);
// alert("x:" + inputSystem.mouseX + " y:" + inputSystem.mouseY);
}
function onMouseUp(e)
{
if(game_state.onMouseUp != undefined)
game_state.onMouseUp(e);
}
function ChangeGameState(nextGameState)
{
if(nextGameState.Init == undefined)
return;
if(nextGameState.Update == undefined)
return;
if(nextGameState.Render == undefined)
return;
game_state = nextGameState;
game_state.Init();
}
function GameUpdate()
{
timerSystem.Update();
**game_state.Update();**
debugSystem.UseDebugMode();
}
function GameRender()
{
var theCanvas = document.getElementById("GameCanvas");
var Context = theCanvas.getContext("2d");
game_state.Render();
if(debugSystem.debugMode)
{
Context.fillStyle = "#ffffff";
Context.font = '15px Arial';
Context.textBaseline = "top";
Context.fillText("fps: "+ frameCounter.Lastfps, 10, 10);
}
}
function gameLoop()
{
game_state = after_loading_state;
GameUpdate();
GameRender();
frameCounter.countFrame();
}
RS_Title.js
function TitleState()
{
this.imgBackground = resourcePreLoader.GetImage("/.c9/img/title_background.png");
this.imgButtonStartOff = resourcePreLoader.GetImage("/.c9/img/title_start_off.png");
this.imgButtonStartOn = resourcePreLoader.GetImage("/.c9/img/title_start_on.png");
this.imgButtonRankingOff = resourcePreLoader.GetImage("/.c9/img/title_ranking_off.png");
this.imgButtonRankingOn = resourcePreLoader.GetImage("/.c9/img/title_ranking_on.png");
soundSystem.PlayBackgroundMusic("/.c9/background.mp3");
return this;
}
TitleState.prototype.Init = function()
{
soundSystem.PlayBackgroundMusic("/.c9/background.mp3");
};
TitleState.prototype.Render = function()
{
var theCanvas = document.getElementById("GameCanvas");
var Context = theCanvas.getContext("2d");
Context.drawImage(this.imgBackground, 0, 0);
//drawing button
if(inputSystem.mouseX > 170 && inputSystem.mouseX < 170+220
&& inputSystem.mouseY > 480 && inputSystem.mouseY < 480+100)
{
Context.drawImage(this.imgButtonStartOn, 170, 480);
this.flagButtonStart = true;
}
else
{
Context.drawImage(this.imgButtonStartOff, 170, 480);
this.flagButtonStart = false;
}
if(inputSystem.mouseX > 420 && inputSystem.mouseX < 420+220
&& inputSystem.mouseY > 480 && inputSystem.mouseY < 480+100)
{
Context.drawImage(this.imgButtonRankingOn, 420, 480);
this.flagButtonRanking = true;
}
else
{
Context.drawImage(this.imgButtonRankingOff, 420, 480);
this.flagButtonRanking = false;
}
};
TitleState.prototype.Update = function()
{
};
TitleState.prototype.onMouseDown = function()
{
if(this.flagButtonStart)
ChangeGameState(new PlayGameState());
after_loading_state = PlayGameState;
game_state = PlayGameState;
if(this.flagButtonRanking)
ChangeGameState();
};
RS_PlayGame.js
function PlayGameState()
{
this.imgBackgroundSky = resourcePreLoader.GetImage("/.c9/img/game_background_sky.png");
}
PlayGameState.prototype.Init = function()
{
var theCanvas = document.getElementById("GameCanvas");
var Context = theCanvas.getContext("2d");
Context.drawImage(this.imgBackgroundSky, 0, 0);
};
PlayGameState.prototype.Render = function()
{
var theCanvas = document.getElementById("GameCanvas");
var Context = theCanvas.getContext("2d");
Context.drawImage(this.imgBackgroundSky, 0, 0);
};
PlayGameState.prototype.Update = function()
{
var theCanvas = document.getElementById("GameCanvas");
var Context = theCanvas.getContext("2d");
Context.drawImage(this.imgBackgroundSky, 0, 0);
};
As mentioned by the others, in the onMouseDown method you are assigning after_loading_state and game_state to PlayGameState which is a function and not an object. So later on when you want to access the Update method, it simply doesn't exist, because it is defined over the object prototype and not the function. You might want to do something like this so that you also avoid instantiating (calling) PlayGameState multiple times:
game_state = new PlayGameState();
ChangeGameState(game_state);
after_loading_state = game_state;
This question already has answers here:
How to access the correct `this` inside a callback
(13 answers)
Closed 6 years ago.
I am doing some excercise, and I have issue with losing "this" context in a pipe method:
function Main() {
// something
this.render = function () {
this.groups.forEach(function(g){
renderGroup(g);
});
return this;
};
// something
this.pipe = function () {
this.render(); // 1
requestAnimationFrame(this.pipe); // 2
return this;
};
// something
}
Ad.1: that cause "this is undefined, so it has no render function"
Ad.2: if above commented, still "this" context is undefined so pipe is not a function
initialization:
function init () {
var m = new Main();
requestAnimationFrame(m.pipe);
}
window.onload = function () {
init();
}
full object code:
function Main() {
this.canvas = document.createElement("canvas");
this.canvas.width = 1366;
this.canvas.height = 768;
this.canvas.style.width = this.canvas.width + "px";
this.canvas.style.height = this.canvas.height + "px";
this.groups = [];
window.app = {};
this.grain = 30 * 1000 * 60;
this.grainWidth = 30;
this.getGroups = function () {return this.groups;}
this.render = function () {
this.groups.forEach(function(g){
renderGroup(g);
});
return this;
};
this.ctx = this.canvas.getContext("2d");
this.pipe = function () {
this.render();
requestAnimationFrame(this.pipe);
return this;
};
document.body.appendChild(this.canvas);
registerEvents();
}
the renderGroup is plain forEach.
What causes the context lost?
Simply bind the context you want pipe to be called with
this.pipe = function () {
this.render(); // 1
requestAnimationFrame(this.pipe); // 2
return this;
}.bind(this)
Maybe something like this?
requestAnimationFrame(this.pipe.bind(this));
JavaScript functions get this defined when they're called. You use pipe as callback, so the context for it becomes window since it's called from window.requestAnimationFrame.
I need to create an interval wrapper to track if it has been cleared.
The number of parameters to pass to the interval callback should be variable. So this is the code (not working) I implemented to test it:
function MyInterval() {
var id = setInterval.apply(this, arguments); // NOT VALID!!
this.cleared = false;
this.clear = function() {
this.cleared = true;
clearInterval(id);
};
}
var x = 2;
var y = 3;
var fn = function() {
x = x + y;
console.log(x);
};
var interval = new MyInterval(fn, 5000, x, y);
Within the call to setInterval, this must refer to the global object, so instead of this, you want window in your constructor:
var id = setInterval.apply(window, arguments);
// here -------------------^
(or in loose mode you could use undefined or null.)
Then it works, at least on browsers where setInterval is a real JavaScript function and therefore has apply:
function MyInterval() {
var id = setInterval.apply(window, arguments);
this.cleared = false;
this.clear = function() {
this.cleared = true;
clearInterval(id);
};
}
var x = 2;
var y = 3;
var fn = function() {
x = x + y;
log(x);
};
var interval = new MyInterval(fn, 500, x, y);
setTimeout(function() {
interval.clear();
}, 3000);
function log(msg) {
var p = document.createElement('p');
p.appendChild(document.createTextNode(msg));
document.body.appendChild(p);
}
Note, though, that host-provided functions are only required to be callable, they are not required to inherit from Function.prototype and so they're not required/guaranteed to have apply. Modern browsers ensure they do, but earlier ones (IE8, for instance) did not. I can't speak to how well-supported apply is on setInterval.
If you need to support browsers that may not have it, just to use your own function:
function MyInterval(handler, interval) {
var args = Array.prototype.slice.call(arguments, 2);
var tick = function() {
handler.apply(undefined, args);
};
var id = setInterval(tick, interval);
this.cleared = false;
this.clear = function() {
this.cleared = true;
clearInterval(id);
};
}
This also has the advantage that it works even on browsers that don't support additional args on setInterval (fairly old ones).
Example:
function MyInterval(handler, interval) {
var args = Array.prototype.slice.call(arguments, 2);
var tick = function() {
handler.apply(undefined, args);
};
var id = setInterval(tick, interval);
this.cleared = false;
this.clear = function() {
this.cleared = true;
clearInterval(id);
};
}
var x = 2;
var y = 3;
var fn = function() {
x = x + y;
log(x);
};
var interval = new MyInterval(fn, 500, x, y);
setTimeout(function() {
interval.clear();
}, 3000);
function log(msg) {
var p = document.createElement('p');
p.appendChild(document.createTextNode(msg));
document.body.appendChild(p);
}
You might be tempted to use the new ES2015 spread operator:
var id = setInterval(...arguments);
...but note that if you transpile (and right now you'd have to), it ends up being an apply call, and so you have the issue of whether apply is supported.
I suggest that you pass an "options" parameter to your timeout.
var MyInterval = (function(window) {
return function(callbackFn, timeout, options) {
var id = setInterval.apply(window, arguments);
this.cleared = false;
this.clear = function() {
this.cleared = true;
clearInterval(id);
};
}
}(window));
var fn = function(opts) {
opts.x += opts.y;
console.log('x = ', opts.x);
};
var opts = {
x: 2,
y: 3
};
var ms = 5000;
var interval = new MyInterval(fn, ms, opts);
// Bootstrap a custom logger. :)
console.log = function() {
var logger = document.getElementById('logger');
var el = document.createElement('LI');
el.innerHTML = [].join.call(arguments, ' ');
logger.appendChild(el);
logger.scrollTop = logger.scrollHeight;
}
body{background:#7F7F7F;}h1{background:#D7D7D7;margin-bottom:0;padding:0.15em;border-bottom:thin solid #AAA;color:#444}#logger{height:120px;margin-top:0;margin-left:0;padding-left:0;overflow:scroll;max-width:100%!important;overflow-x:hidden!important;font-family:monospace;background:#CCC}#logger li{list-style:none;counter-increment:step-counter;padding:.1em;border-bottom:thin solid #E7E7E7;background:#FFF}#logger li:nth-child(odd){background:#F7F7F7}#logger li::before{content:counter(step-counter);display:inline-block;width:1.4em;margin-right:.5em;padding:.25em .75em;font-size:1em;text-align:right;background-color:#E7E7E7;color:#6A6A6A;font-weight:700}
<h1>Custom HTML Logger</h1><ol id="logger"></ol>
I created a utility function rather than a constructor to solve your issue.
function Wrapper(delay) {
var isCleared,
intervalId,
intervalDelay = delay || 5e3; // default delay of 5 sec
function clear() {
if (!isCleared) {
console.log('clearing interval');
isCleared = true;
clearInterval(intervalId);
}
}
function setUpInterval(callback){
var params = [].slice.call(arguments, 1);
if (!callback) {
throw new Error('Callback for interval expected');
}
params.unshift(intervalDelay);
params.unshift(callback);
intervalId = setInterval.apply(null, params);
}
return {
setUp : setUpInterval,
clear : clear
}
}
function intervalCallback() {
console.log([].slice.call(arguments).join(','));
}
var wrapper = Wrapper(1e3); // create wrapper with delay for interval
console.log('test case 1');
wrapper.setUp(intervalCallback, 'params', 'to', 'callback');
// call clear interval after 10sec
setTimeout(function() {
wrapper.clear();
}, 10e3);
Hope this helps.
While trying to create a mouse listener to canvas object I faced a problem which took me a long time to solve - How can I pass object variables (this.X, this.Y) to an event listener, for example:
function Test() {
this.canvas = ....
this.mouseDownHandler = ....
canvas.addEventLIstener('mousedown', this.mouseDownListener, false);
}
So I came up with the following solution
This is the solution the worked for me -
function Test() {
this.ctx = this.canvas.getContext("2d");
var self = this;
this.canvas.addEventListener("mousedown",
function(e, param) {
self.mouseDownHandler(e, param);
}.bind(null, this), false);
}
Test.prototype.mouseDownHandler = function(t, e) {
t.ctx.fillRect(e.pageX, e.pageY, 10, 10);
};
If you don't mind me slightly simplifying #Yehonatan 's answer:
class Test {
constructor() {
this.canvas = document.getElementById("app")
this.ctx = this.canvas.getContext("2d")
this.canvas.addEventListener("mousedown", e => this.mouseDownHandler(e))
}
mouseDownHandler(e) {
this.ctx.fillRect(e.pageX, e.pageY, 10, 10)
}
}
let t = new Test()
JSFiddle example
I was playing around with making game in JS. And hit a brick wall which is inability to change a variable from an event in the main html file. Namely speaking offSetX. Why it doesn't change?
var game = new Game();
window.addEventListener("keyup", game.input);
game.start('myCanvas');
The game object looks like this:
function Game() {
this.offSetX = 0;
this.init = function (id) {
this.canvas = document.getElementById(id);
this.context = this.canvas.getContext('2d');
this.blocks = [];
this.blocks.push(new block());
};
this.logic = function () {
for (var i in this.blocks) {
this.blocks[i].update(this.offSetX);
}
};
this.draw = function () {
for (var i in this.blocks) {
this.blocks[i].draw(this.context);
}
};
this.main = function () {
this.logic();
this.draw();
console.log(this.offSetX);
};
this.input = function (key) {
if (key.keyCode == 37) {
this.offSetX--;
console.log(this.offSetX);
}
if (key.keyCode == 39) {
this.offSetX++;
console.log(this.offSetX);
}
};
this.start = function (id) {
var _this = this;
this.init(id);
this.interval = setInterval(function () {
_this.canvas.width = _this.canvas.width;
_this.main();
}, 30);
}
};
Try this:
window.addEventListener("keyup", function(key){
game.input.apply(game,[key]);
});
The problem was by window.addEventListener("keyup", game.input) line , you are adding handler for window object, that's why in input method , "this" is window object(which does not have any "offSetX" method), not the game object.