boagz57

Pitfalls of pixels as unit in game

Recommended Posts

So I've been trying to develop my own little c++, 2d fighting game from scratch and I'm hung up on what exactly I'm suppose to use as 'units' for my game. Currently, as suggested here on another game dev site, my game screen is thought of as a 160x90 grid. So if my game is being run at 1080p screen resolution, that would equal exactly 12 pixels per 1 grid unit (which I call a world unit). For example, I will have code in my game where I will tell my sprite to move 10.0 units/sec in the positive x direction. This means at 1080p my sprite will move 120 pixels/sec when run (12 pixels/unit * 10 units). 

Recently though, I've started trying to add some collision detection to my game and I've run into a bit of an issue with when to convert my world units into pixels and getting my collisions to work properly. While I'm sure I can fix this after some time, this got me thinking what exactly are the problems with just assuming a certain screen resolution and working strictly with pixels in my game? So instead of saying 'move my sprite 10 game units/sec' just say 'move my sprite 100pixels/sec', avoiding and unit conversion techniques. Then, if I want to, I can come up with some way just to upscale to higher res images or whatever for denser screen resolutions. The reason I ask is on a lot of forums people seem to be against using pixels as units when it comes to collision and physics, even for 2d games.

Share this post


Link to post
Share on other sites

Do never ever convert your pixels to units or vice versa for any math computations! Thats a rule of thumb.

Just use your world units directly, which should be float or double. Integer is not great when you want to smooth movement.

The conversion for world to screen units is only done in rendering or for screen to world conversions (mouse picking for example). You can precalculate a factor which you can multiply your world coordinates to convert to screen coordinates and vice versa.

Building up your world from tiles/grid is totally fine and i do this too like this:

static constexpr f32 TILE_SIZE = 0.5f;
static constexpr u32 TILE_COUNT_FOR_WIDTH = 40;
static constexpr u32 TILE_COUNT_FOR_HEIGHT = 22;
static constexpr f32 GAME_ASPECT = TILE_COUNT_FOR_WIDTH / (f32)TILE_COUNT_FOR_HEIGHT;
static constexpr f32 GAME_WIDTH = TILE_COUNT_FOR_WIDTH * TILE_SIZE;
static constexpr f32 GAME_HEIGHT = GAME_WIDTH / GAME_ASPECT;
static constexpr f32 HALF_GAME_WIDTH = GAME_WIDTH * 0.5f;
static constexpr f32 HALF_GAME_HEIGHT = GAME_HEIGHT * 0.5f;

Also my worlds are always built in a right handed coordinate system - which is the same as opengl. So negative Y is going down and positive Y is going up and half the screen dimension is added to X and Y so the world coordinate 0, 0 is always the center of the screen - without taking any translation into account of course.

Keep in mind, that you need to take the aspect ratio into account as well - so you should letterbox your game view, so it will fit on any display. I do this for every frame once and store the result:

// Calculate a letterboxed viewport offset and size
// @NOTE: Scale is used later for doing unprojection
viewSize = Vec2f(halfGameWidth, halfGameHeight) * 2.0f;
viewScale = (f32)windowSize.w / (halfGameWidth * 2.0f);
Vec2i viewportSize = Vec2i(windowSize.w, (u32)(windowSize.w / aspectRatio));
if (viewportSize.h > windowSize.h) {
  viewportSize.h = windowSize.h;
  viewportSize.w = (u32)(viewportSize.h * aspectRatio);
  viewScale = (f32)viewportSize.w / (halfGameWidth * 2.0f);
}
Vec2i viewportOffset = Vec2i((windowSize.w - viewportSize.w) / 2, (windowSize.h - viewportSize.h) / 2);
viewport.size = viewportSize;
viewport.offset = viewportOffset;

// Update view projection
Mat4f proj = Mat4f::CreateOrthoRH(-halfGameWidth, halfGameWidth, -halfGameHeight, halfGameHeight, 0.0f, 1.0f);
Mat4f model = Mat4f::Identity;
viewProjection = proj * model;

 

Edited by Finalspace

Share this post


Link to post
Share on other sites

It depends on the engine.

The first pitfall to using pixels directly is that, of course, you can't easily change the resolution. However, this might not necessarily be true if you can just set the old pixels as "world units" with little hassle. So that's going to depend on the engine.

The second possible pitfall is that pixels are by definition integers; you can't have a real half-pixel, so you can't be at pixel position 4.2. This is of course imprecise. But it's trivial to just keep pixel positions as floating-point and round or truncate them at display time. It's just a more simplistic "world units" conversion at that point, really. The engine itself can even transparently handle it.

Personally, I prefer to define the game at a particular resolution and just use pixels for distances and movement and such, for two reasons:

  1. It's more simple to do it that way. It means there's no need to worry about whether I'm talking about the display or the game world; it's all the same.
  2. Changing the resolution would involve changing each individual image anyway, so making it easier to code that change would be of little consequence as I see it.

Of course, this is for 2-D games that use raster graphics. If you use vector graphics or 3-D graphics, I definitely see merit in not using pixels as a measurement; after all, you're not working with pixels in that case.

Share this post


Link to post
Share on other sites

Also consider fixed point (you can for example divide a pixel into 256 units).

If you are using pixel perfect sprites there is more of a reason to use some kind of pixel unit, whereas with pure 3d there isn't such a concept as a pixel, so the pixel may not make sense, but you still may choose to use integers.

There are benefits to both floats and ints. Although floats are often seen as the 'default choice' these days, integers are good for consistent behaviour, compression, packing, some people use them for units on large scale maps.

Share this post


Link to post
Share on other sites

@JulieMaru-chan 

3 hours ago, JulieMaru-chan said:

Personally, I prefer to define the game at a particular resolution and just use pixels for distances and movement and such

What about for things like collision detection and physics? Do pixels still work out okay for these kinds of calculations?

@lawnjelly

1 hour ago, lawnjelly said:

If you are using pixel perfect sprites there is more of a reason to use some kind of pixel unit

What about for a 2d game using vector graphics?

Share this post


Link to post
Share on other sites

Physics and collision response can work okay.  You'll likely end up using your own internal representations, but it won't hurt anything.

Also note that many games people think of as pixel based have higher precision internally.  For example, the original Super Mario Bros had 8 bits of sub-pixel data for position and speed, which enabled variations in running versus walking. Visually players saw the player moving a pixel or not, but internally there was more data.

That is, I think, what was being referred to earlier in the discussion as fixed-point data. 

What you see on the screen does not need to precisely match what the simulator is doing internally.  Games tend to keep far more information than is shown to the player, and tend to keep information at a higher precision than the player thinks about.

Share this post


Link to post
Share on other sites
8 hours ago, frob said:

That is, I think, what was being referred to earlier in the discussion as fixed-point data. 

Yes, on our 8/16/32/64 bit computers it makes far more sense to divide a pixel into 256 then say, 100, because you can use bitshifts, bitmasks etc. The fixed point can refer to the number of bits given to the fraction and the number to the whole number, instead of referring to the position of a decimal point.

http://x86asm.net/articles/fixed-point-arithmetic-and-tricks/

Share this post


Link to post
Share on other sites
8 hours ago, frob said:

What you see on the screen does not need to precisely match what the simulator is doing internally.  Games tend to keep far more information than is shown to the player, and tend to keep information at a higher precision than the player thinks about.

While rounding high precision positions to whole pixels for rendering purposes only should be the default strategy to deal with pixels (both for fixed point and for floating point) in some cases deliberately and selectively quantizing positions, times, velocities etc. in the game engine might make sense.

For example, suppose you need to execute a long jump to a ledge in a platformer or to run into a narrow opening in a racing or flying game: the player cannot tell whether his sprite is actually aligned with the pixel grid or slightly off, and therefore basing success or failure (through high precision collision detection) on unavailable information would be unfair.

Share this post


Link to post
Share on other sites
5 hours ago, LorenzoGatti said:

For example, suppose you need to execute a long jump to a ledge in a platformer or to run into a narrow opening in a racing or flying game: the player cannot tell whether his sprite is actually aligned with the pixel grid or slightly off, and therefore basing success or failure (through high precision collision detection) on unavailable information would be unfair.

That's not going to factor in to this realistically. No human can possibly be precise enough to be pixel-perfect on a consistent basis anyway. It might even literally be impossible if the speed is too high. So I wouldn't worry about an edge case such as this.

Share this post


Link to post
Share on other sites

The edge case of caring for pixel-perfect positioning is fairly common in old, low resolution games where 1 pixel is actually a large distance and 1 frame is a long time. When I refer to racing or flying games I mean things like aiming for ramps in Micro Machines or bullet gaps in Space Invaders, not forgiving 3D games.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now


  • Announcements

  • Forum Statistics

    • Total Topics
      628329
    • Total Posts
      2982103
  • Similar Content

    • By Connor Rust
      I am currently attempting to make a navigation mesh for our 2D top down game, which is a multiplayer game using Node.js as the server communication. At the moment, I have implemented A* over an obstacle hardnessmap, which is awfully slow and laggy at times when we test our game on Heroku. I have been trying to find an algorithm to automatically generate the navmesh after map creation, instead of me having to do this manually. I am currently attempting to use Delaunay's Triangulation Divide and Conquer algorithm, but I am running into some issues. I have already asked a question on StackOverflow and am not getting many suggestions and help from it, so I figured I would come here. Is there another algorithm that might be better to use for the navmesh generation in comparison to Deluanay's Triangulation? My current implementation seems extremely buggy during the merge step and I cannot find the error. I have checked over the code countless times, comparing it to the description of the algorithm from http://www.geom.uiuc.edu/~samuelp/del_project.html. 
      My current code is this:
      class MapNode { constructor(x, y) { this.position = new Vector(x, y); this.neighbors = []; } distance(n) { return this.position.distance(n.position); } inNeighbor(n) { for (let i = 0; i < this.neighbors.length; i++) { if (this.neighbors[i] === n) return true; } return false; } addNeighbor(n) { this.neighbors = this.neighbors.filter((node) => node != n); this.neighbors.push(n); } addNeighbors(arr) { let self = this; arr.forEach((n) => self.neighbors.push(n)); } removeNeighbor(n) { this.neighbors = this.neighbors.filter((neighbor) => neighbor != n); } } class Triangle { constructor(p1, p2, p3) { this.p1 = p1; this.p2 = p2; this.p3 = p3; this.neighbors = []; } addNeighbors(n) { this.neighbors.push(n); } } function genSubMat(matrix, ignoreCol) { let r = []; for (let i = 0; i < matrix.length - 1; i++) { r.push([]); for (let j = 0; j < matrix[0].length; j++) { if (j != ignoreCol) r[i].push(matrix[i + 1][j]); } } return r; } function determinantSqMat(matrix) { if (matrix.length != matrix[0].length) return false; if (matrix.length === 2) return matrix[0][0] * matrix[1][1] - matrix[1][0] * matrix[0][1]; let det = 0; for (let i = 0; i < matrix.length; i++) { let r = genSubMat(matrix, i); let tmp = matrix[0][i] * determinantSqMat(r); if (i % 2 == 0) det += tmp; else det -= tmp; } return -det; } // if d is in the circle formed by points a, b, and c, return > 0 // d is on circle, return 0 // d is outside of circle, return < 0 function inCircle(a, b, c, d) { let arr = [a, b, c, d]; let mat = [ [], [], [], [] ]; for (let i = 0; i < arr.length; i++) { mat[i][0] = 1; mat[i][1] = arr[i].position.x; mat[i][2] = arr[i].position.y; mat[i][3] = arr[i].position.x * arr[i].position.x + arr[i].position.y * arr[i].position.y; } return determinantSqMat(mat); } function walkable(from, to, hardnessMap) { let diff = new Vector(to.x - from.x, to.y - from.y); if (Math.abs(diff.x) > Math.abs(diff.y)) diff.scale(Math.abs(1 / diff.x)); else diff.scale(Math.abs(1 / diff.y)); let current = new Vector(from.x + diff.x, from.y + diff.y); while (Math.round(current.x) != to.x || Math.round(current.y) != to.y) { if (hardnessMap[Math.floor(current.y)][Math.floor(current.x)] === 1) return false; current.x += diff.x; current.y += diff.y; } return true; } function getLowest(nodes) { let lowest = nodes[0]; for (let i = 1; i < nodes.length; i++) { if (nodes[i].position.y < lowest.position.y) lowest = nodes[i]; } return lowest; } // returns the angle between 2 vectors, if cw is true, then return clockwise angle between, // else return the ccw angle between. b is the "hinge" point function angleBetween(a, b, c, cw) { let ba = new Vector(a.position.x - b.position.x, a.position.y - b.position.y); let bc = new Vector(c.position.x - b.position.x, c.position.y - b.position.y); let v0 = new Vector(0, 1); let angleBA = v0.angleBetween(ba) * 180 / Math.PI; if (angleBA < 0) angleBA += 360; let angleBC = v0.angleBetween(bc) * 180 / Math.PI; if (angleBC < 0) angleBC += 360; let smallest = Math.min(angleBA, angleBC); let largest = Math.max(angleBA, angleBC); let angle = largest - smallest; return (cw) ? angle : 360 - angle; } function sortSmallestAngle(a, b, list, cw) { list.sort((m, n) => { let vab = new Vector(a.position.x - b.position.x, a.position.y - b.position.y); let vmb = new Vector(m.position.x - b.position.x, m.position.y - b.position.y); let vnb = new Vector(n.position.x - b.position.x, n.position.y - b.position.y); if (cw) return vab.angleBetween(vmb, cw) - vab.angleBetween(vnb, cw); else return vab.angleBetween(vnb, cw) - vab.angleBetween(vmb, cw); }); } // a is in list, b is in the other list function getPotential(a, b, list, cw) { sortSmallestAngle(b, a, list, cw); for (let i = 0; i < list.length - 1; i++) { let angle = angleBetween(b, a, list[i], cw); if (angle > 180) return false; else if (inCircle(a, b, list[i], list[i + 1]) <= 0) return list[i]; else { a.removeNeighbor(list[i]); list[i].removeNeighbor(a); } } let potential = list[list.length - 1]; if (potential) { let angle = angleBetween(a, b, potential, cw); if (angle > 180) return false; return potential; } return false; } function merge(leftNodes, rightNodes, leftBase, rightBase, hardnessMap) { leftBase.addNeighbor(rightBase); rightBase.addNeighbor(leftBase); let newLeft = leftNodes.filter((n) => n != leftBase); let newRight = rightNodes.filter((n) => n != rightBase); let potentialLeft = getPotential(leftBase, rightBase, newLeft, false); let potentialRight = getPotential(rightBase, leftBase, newRight, true); if (!potentialLeft && !potentialRight) return; else if (potentialLeft && !potentialRight) merge(newLeft, newRight, potentialLeft, rightBase, hardnessMap); else if (potentialRight && !potentialLeft) merge(newLeft, newRight, leftBase, potentialRight, hardnessMap); else { if (inCircle(leftBase, rightBase, potentialLeft, potentialRight) <= 0) merge(newLeft, newRight, potentialLeft, rightBase, hardnessMap); if (inCircle(leftBase, rightBase, potentialRight, potentialLeft) <= 0) merge(newLeft, newRight, leftBase, potentialRight, hardnessMap); } } // divide and conquer algorithm function delaunay(nodes, hardnessMap) { if (nodes.length <= 3) { for (let i = 0; i < nodes.length; i++) for (let j = 0; j < nodes.length; j++) if (i != j) nodes[i].addNeighbor(nodes[j]); return nodes; } else { nodes.sort((a, b) => { let tmp = a.position.x - b.position.x; if (tmp === 0) return b.position.y - a.position.y; return tmp; }); let l = nodes.length; let leftNodes; let rightNodes; if (l === 4) { leftNodes = delaunay(nodes.slice(0, 3), hardnessMap); rightNodes = delaunay(nodes.slice(3, 4), hardnessMap); } else { leftNodes = delaunay(nodes.slice(0, Math.floor(nodes.length / 2)), hardnessMap); rightNodes = delaunay(nodes.slice(Math.floor(nodes.length / 2), nodes.length), hardnessMap); } let leftBase = getLowest(leftNodes); let rightBase = getLowest(rightNodes); merge(leftNodes, rightNodes, leftBase, rightBase, hardnessMap); console.log("=============================MergeComplete================================"); return nodes; } }  
    • By Hilster
      Hello 2D Artists,
      I've started making a 2D Puzzle Adventure game for mobile and I'm looking for someone who would want in on creating assets for the game. The core of the programming is pretty much complete, you can walk within the grid laid out and push boxes, when there is an object on top of a pressure pad it will activate the linked objects or if there is one object with multiple linked pressure pads it requires you to activate all points for the object to become active. 

      The level iteration for the game is quick and simple, a Photoshop file that is made of individual pixels that represents objects is put into the game and it creates the level out of those pixels with the assigned objects.
      The objects that need sprites created so far is the character, box, pressure pad, door, trap door, the walls, the stairs and the tiled background.
      I intend to add more objects so the amount I'd like to add will be extended.
      My motivations for posting here is to have something that looks nice to be able to display on my portfolio, so if you're looking for a working game that you can place your art into and improve the look of your portfolio then we're in business.
      Please reply with a few past examples of your art below and I'll be in touch!
    • By Dyla24
      Hello  
      I am currently doing a study into Persuasive Games and how the design of them could be improved to make them more efficient. 
      While looking for places to post my survey I found this forum and hoped you guys could give me some thoughts and opinions  
      If you are interested in participating in the survey you can find the link here:
      https://goo.gl/forms/6LszmtYnPd3TjgpF2
      Also please let me know what you think of the genre and how you think the design could be improved ? 
      Thank you =) 
    • By suliman
      Hi!
      My game is coming along nicely and I would love some feedback.
      You play as one (or two in co-op) survivor that must travel the land and survive the infected hordes, looners and bandits. You stop in locations but are always pressed as the hordes will start pouring in. Collect resources (food, fuel, medical supplies and ammo) and weapons and head for the goal!
      Tips
      Always quickly switch to a melee weapon if running out of ammo Loot everything if you have time to loot, including cartrunks Choose locations that have the loot you need (such as gas station for fuel) Try to avoid running out of fuel or having your car break down. Walking is dangerous! Download (50 MB, works with windows only, you DON'T need dropbox to download):
      Damnation road (beta 2)




  • Popular Now