Reputation: 371
I'm trying Ebitengine (Golang) as a hobby project to write some very simple game: giving some images, whenever some part is clicked, it will disappear.
However, in the tutorial, the engine developer says:
Use alphaImage (*image.Alpha) instead of image (*ebiten.Image) here. It is because (*ebiten.Image). At is very slow as this reads pixels from GPU, and should be avoided whenever possible.
Clone an image but only with alpha values. This is used to detect a user cursor touches the image.
// (Example code)
b := img.Bounds()
ebitenAlphaImage = image.NewAlpha(b)
for j := b.Min.Y; j < b.Max.Y; j++ {
for i := b.Min.X; i < b.Max.X; i++ {
ebitenAlphaImage.Set(i, j, img.At(i, j))
}
}
... but in my case, my example image is like this:
... and I want to click on some organ and it will disappear (just a showcase...), so if I just cut the image in photoshop and hide corresponding layer, all the images will be in the same size, and such "alpha image" solution wouldn't work, because all images have the same size/position.
So, currently, my solution is to just loop through all images on every click, and check which one's corresponding Alpha value at the cursor is not empty:
package main
import (
"fmt"
"image/color"
_ "image/png"
"log"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
// var example_img *ebiten.Image
var body *ebiten.Image
var digest *ebiten.Image
var head *ebiten.Image
var heart *ebiten.Image
var lung *ebiten.Image
var parts map[string]*ebiten.Image
var hide map[string]bool
func init() {
var errs []error
var temp_err error
// https://ebitengine.org/en/tour/image.html
body, _, temp_err = ebitenutil.NewImageFromFile("img/body.png")
errs = append(errs, temp_err)
digest, _, temp_err = ebitenutil.NewImageFromFile("img/digest.png")
errs = append(errs, temp_err)
head, _, temp_err = ebitenutil.NewImageFromFile("img/head.png")
errs = append(errs, temp_err)
heart, _, temp_err = ebitenutil.NewImageFromFile("img/heart.png")
errs = append(errs, temp_err)
lung, _, temp_err = ebitenutil.NewImageFromFile("img/lung.png")
errs = append(errs, temp_err)
parts = map[string]*ebiten.Image{
"body": body,
"digest": digest,
"head": head,
"heart": heart,
"lung": lung,
}
hide = map[string]bool{
"body": false,
"digest": false,
"head": false,
"heart": false,
"lung": false,
}
is_err := false
for _, err := range errs {
if err != nil {
log.Fatal(err)
is_err = true
}
}
if is_err {
log.Fatal("Error loading images")
}
/*
var err error
example_img, _, err = ebitenutil.NewImageFromFile("img/model.png")
if err != nil {
log.Fatal(err)
}
*/
}
type Game struct{}
// Tick, called 60 times per second (default)
func (g *Game) Update() error {
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
x, y := ebiten.CursorPosition()
part := CheckPart(x, y)
fmt.Printf("Mouse position: x=%d, y=%d, part=%s\n", x, y, part)
if part != "None" {
hide[part] = true
}
}
return nil
}
// Called every frame (depending on the monitor refresh rate).
// Screen argument is the final destination of rendering.
// A ebitenutil.DebugPrint is a utility function to render a debug message on an image.
func (g *Game) Draw(screen *ebiten.Image) {
// Print debug message on the screen
// ebitenutil.DebugPrint(screen, "Hello, World!")
// Fill the screen with a color
screen.Fill(color.RGBA{0xff, 0xff, 0xff, 0xff})
// Draw the image on the screen
// screen.DrawImage(example_img, nil)
for name, part := range parts {
if !hide[name] {
screen.DrawImage(part, nil)
}
}
}
// Layout accepts an outside size, which is a window size on desktop, and returns the game's logical screen size.
// Layout will be more meaningful e.g., when the window is resizable.
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return 640, 480
}
func main() {
ebiten.SetWindowSize(640, 480)
ebiten.SetWindowTitle("Demo")
// Run the main loop of an Ebitengine game
if err := ebiten.RunGame(&Game{}); err != nil {
log.Fatal(err)
}
}
func CheckPart(posX int, posY int) string {
// Check alpha value of all parts at the given position
// If alpha value is not 0, return the part name
// Otherwise, return an empty string
for name, part := range parts {
// Get the color of the pixel at the given position
_, _, _, alpha := part.At(posX, posY).RGBA()
// Check if the alpha value is not 0
if alpha != 0 {
return name
}
}
return "None"
}
I want to know whether there are any better solutions? Or I have to make each part in different size, and place them precisely in the image in real case?
Upvotes: 3
Views: 254
Reputation: 526
If you want to follow the linked tutorial more closely and keep your organs as overlapping images of the same size and position, you can consider making a Sprite struct which stores the *ebiten.Image as well as the *image.Alpha. The only difference between this code and your code will be detecting via the *image.Alpha instead of the *ebiten.Image.At.
type Sprite struct {
image *ebiten.Image
alphaImage *image.Alpha
// x and y could be stored here, or just constants if the "body" will never move
x int
y int
}
The alphaImage can be cloned via the code snippet you shared. Then your In method can check whether the cursor is on an organ via its alphaImage:
func (s *Sprite) In(x, y int) bool {
return s.alphaImage.At(x-s.x, y-s.y).(color.Alpha).A > 0
}
Your body will have one Sprite per organ and since the pixel alpha is only nonzero on the pixels of that organ this will return true if the mouse is over that organ. Then your loop will be modified to
func CheckPart(posX int, posY int) string {
// Check alpha value of all parts at the given position
// If alpha value is not 0, return the part name
// Otherwise, return an empty string
for name, part := range parts {
if part.In(posX, posY) {
return name
}
}
return nil
}
Upvotes: 0