|
I'm trying to write a 2d game (remastered version of Digger from 1983 - can be played online here: https://www.futrega.org/digger/) My question is about the field that can change (it can be eaten by the digger and hobbins, destroyed by falling bags, etc.). It is also used for collision detection to make monsters move along the eaten corridors. So my field is: I start with: Then I draw the background in two loops: Then I draw corridors like this: Then I finish: So I draw black textures onto the field to draw tunnels. Then I draw this field texture onto the screen texture. And it all works nicely (drawing). But now I need to make monsters move around and they need to know where black is (eaten field). On one forum I read what Ray wrote:
This is the use-case. I could use But I'm dealing with textures. Should I draw to an image and then just use this sequence on frame change? as in this example textures_image_processing ? How efficient is this? Or should I change the field both in a texture and in a in-memory image? |
Replies: 4 comments 1 reply
|
The short version: don't. Ray's warning about retrieving texture data applies to your use case too. The fix is not to find a cheaper readback, it is to stop asking the GPU where the walls are. A render texture lives in GPU memory. Anything that reads it back, The deeper problem is that pixels are the wrong source of truth. Digger's field is a grid, not a bitmap. The original is laid out in tiles and tunnels are carved in whole cells. If you ask the framebuffer whether a pixel is black, you are reconstructing grid state you already had before you drew it, and reconstructing it from something lossy: a black tunnel pixel and a black background pixel are indistinguishable, and any sprite drawn over the field temporarily corrupts the answer. So keep two things that never touch each other. 1. The authoritative field, on the CPU. A plain array, one entry per cell: typedef enum { CELL_EARTH = 0, CELL_TUNNEL, CELL_EMERALD } CellState;
static CellState field[FIELD_ROWS][FIELD_COLS];Digging writes to this array. Collision, monster pathing and "can I move here" all read from it. Lookups are O(1) array indexing with zero driver involvement, so you can run them for every entity every frame without thinking about it. 2. The render texture, purely for display. Keep doing exactly what you are doing now, Both get updated at the same moment, in the one place where digging happens: void CarveTunnel(int row, int col)
{
field[row][col] = CELL_TUNNEL; // logic
BeginTextureMode(fld.target); // presentation
DrawTexture(downBlobTexture, col*CELL_W, row*CELL_H, WHITE);
EndTextureMode();
}That also answers your last question, whether to change the field both in a texture and an in memory image. Nearly, but use a small array of your own enum rather than an One thing you get for free once state is on the CPU: monster AI stops being per pixel. Because tunnels are carved in whole cells, a monster only needs to consider its four neighbouring cells at the moment it reaches a cell centre, which is how the original behaved and why its monsters move on that characteristic grid. Sub pixel collision testing against a bitmap would actually be less faithful to the game you are remastering. The readback route in |
|
Thank you for your elaborated answer! I really appreciate it. Since the time when I asked the question I've done some work which can be seen here: https://geniot.github.io/digger/ The code is here: https://github.com/geniot/digger It's still in the initial phase but the basis is laid. So I guess I'm reverse engineering the game, not remastering it. It's less faithful to the original game. Movement and collisions are pixel perfect. But I'm using a grid for movement: For collision with the field I'm drawing both the texture and the image of the field (one for writing, the other for reading): I even wrote a debug function that compares the pixels of these two:
I'm going to cheat here. If next levels of Digger have black pixels in the field background they are going to be almost black, the difference invisible to the eye. But the first level looks good. Sprites are drawn in layers: https://github.com/geniot/digger/blob/main/src/scene_game.go#L61 , and so they do not corrupt anything. And that is another big difference from the original game - how stuff is drawn. I wonder when a falling bag hits the digger and some monsters along the way how it all is going to look like? In the original game they all start to kind of blink overlapping each other pulled down together. But we'll see what I come up with when it comes to implementing this. Again: I'm not trying to make it 100% faithful, really hoping to make something better, smoother, AI smarter, etc. Playing in the browser, on desktop and a few of my handheld game consoles, such as Trimui: https://github.com/geniot/tsp-rubik I considered creating a bidemensional array for tracking the field changes, I considered arrays for cells, but then I was constantly hitting the same question: if an image is an array itself why would I create another array that duplicates it? I am still not sure that Raylib's image is a good answer to the field collision detection but we'll see. Monster AI is something that I'm going to look at more closely I guess. When it comes to it. Thanks again for your answer! It really motivates me to work on this remaster further. |
|
Your pushback is fair, and on the main point you were already ahead of my answer. You are not reading back from the GPU in the hot path: You are also right to reject the cell grid. I pitched But I would put the remaining question differently. "If an image is an array itself why would I create another array that duplicates it?" The array is not what gets duplicated. The rasterizer is. You now have two independent pieces of code that turn the same intent into pixels, One thing you raised that turns out not to be a problem: I checked There is a real bug in
Color GetImageColor(Image image, int x, int y)
{
Color color = { 0 };
if ((x >=0) && (x < image.width) && (y >= 0) && (y < image.height))
{
...
}
else TRACELOG(LOG_WARNING, "IMAGE: Failed to get pixel data, x/y out of bounds");
return color;
}Your predicate is if x < 0 || y < 0 || x >= FieldWidth || y >= FieldHeight {
return true
}That is also the general shape of the argument for a plain Go array. Not a coarser one, the same pixel resolution you already use: dug [FieldWidth][FieldHeight]bool // exactly like MoveGrid.dotsYou already have precisely this structure in The last one is the reason I would still move off colours eventually, even though your current setup works. Black versus not black is one bit, and the field is about to need more than one. Once emeralds and money bags live in the field, "is this pixel passable" and "is this pixel an emerald" and "is this pixel earth that a bag can fall through" are different questions, and a |
|
That is exactly it, and the laziness instinct was sound. You should not write an eating algorithm. You should write a stamp, which is about fifteen lines. You already keep type TextureImage struct {
image *rl.Image
texture rl.Texture2D
mask [][]bool // new
width float32
height float32
}
// after the rotate/flip block, before or after LoadTextureFromImage
cols := rl.LoadImageColors(textureImage.image)
w, h := int(textureImage.image.Width), int(textureImage.image.Height)
textureImage.mask = make([][]bool, w)
for x := range textureImage.mask {
textureImage.mask[x] = make([]bool, h)
}
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
textureImage.mask[x][y] = cols[y*w+x].A > 127
}
}
rl.UnloadImageColors(cols)
Then the stamp, which takes the source rect so it keeps working for the half blob cases in func (field *Field) carve(mask [][]bool, src rl.Rectangle, dstX, dstY int32) {
sx, sy := int32(src.X), int32(src.Y)
for j := int32(0); j < int32(src.Height); j++ {
fy := dstY + j
if fy < 0 || fy >= FieldHeight {
continue
}
for i := int32(0); i < int32(src.Width); i++ {
fx := dstX + i
if fx < 0 || fx >= FieldWidth {
continue
}
if mask[sx+i][sy+j] {
field.dug[fx][fy] = true
}
}
}
}and it drops in exactly where the rl.DrawTexturePro(textureImage.texture, sourceRect, destRect, ZERO_VECTOR2, 0, rl.White)
field.carve(textureImage.mask, sourceRect, int32(destRect.X), int32(destRect.Y))Three things worth knowing before you run it. The clipping in func (field *Field) IsColliding(rec rl.Rectangle) bool {
x0, y0 := int32(rec.X), int32(rec.Y)
for y := y0; y < y0+int32(rec.Height); y++ {
for x := x0; x < x0+int32(rec.Width); x++ {
if x < 0 || y < 0 || x >= FieldWidth || y >= FieldHeight {
return true
}
if !field.dug[x][y] {
return true
}
}
}
return false
}No flip. Your source and destination rects are the same size at every call site today, so the stamp never has to scale. If you ever draw a blob at a different size than its source rect, that is the one place you would need to care. One correction to something I implied earlier, since I went and looked: If this answers the original question about RenderTexture and collision, marking it as the answer would help the next person who searches for it, since this thread is one of the few places the readback question gets a concrete alternative rather than just "don't do that". |
That is exactly it, and the laziness instinct was sound. You should not write an eating algorithm. You should write a stamp, which is about fifteen lines.
You already keep
imagenext totextureinTextureImage, and you already apply the rotate and the flips beforeLoadTextureFromImage, so the mask can be built inNewTextureImagefrom the same pixels the GPU gets: