Impact

This forum is read only and just serves as an archive. If you have any questions, please post them on github.com/phoboslab/impact

1 decade ago by Heiko

Hey I've uploaded a hitmask plugin to github.
The hitmask is intended for determining hit detection on transparent buttons. The plugin takes the button's image as input, and creates a hitmask (pixel array) with 0's for transparent pixels (hit misses) and 1's for opaque pixels (hits).
You can also set a scaling factor - this will reduce the resulting hitmasks size.

Have a look and let me know if it proves useful to you. Would also appreciate feedback on bugs, and/or ways to improve this plugin.

Get the plugin and documentation here: Impact-ScaledAlphaHitmask-Plugin


Below is an example implementation of a game button using the hitmask plugin (see github repo for more complete example and docs).
* in init() we create an instance of ScaledAlphaHitmask()
* we then set hitmask properties and assign the image to be used to derive hitmask from (the rgba button image)
* then in our hit detection we query the hitmask by passing in relative mouse coordinates. A return value of true indicates a hit, else we got a miss (i.e. mouse event outside of button).

// -------------------------------------------------------------------------------------------------------------
// DemoGamebtn.js
// -------------------------------------------------------------------------------------------------------------
ig.module(
    'game.entities.demogamebtn'
)
.requires(
    'plugins.scaled-alpha-hitmask',
    'plugins.button-state-machine',
    'impact.entity'
)
.defines(function() {
    EntityDemoGamebtn = ig.Entity.extend({
        size: {x: 16, y: 16},

        btnStateMachine: null,
        hitmask: null,

        // -------------------------------------------------------------------------------------------------------------
        // Button States
        // -------------------------------------------------------------------------------------------------------------
        endClick: function() { this.currentAnim = this.anims.btnNormal; },
        endClickIgnore: function() { this.currentAnim = this.anims.btnNormal; },
        endHover: function() { this.currentAnim = this.anims.btnNormal; },
        startClick: function() { this.currentAnim = this.anims.btnDown; },
        startHover: function() { this.currentAnim = this.anims.btnHover; },

        // -------------------------------------------------------------------------------------------------------------
        // Alpha Hit Detection
        // -------------------------------------------------------------------------------------------------------------
        // hittest - returns true if mouse cursor within bounds, else false
        hittest: function() {
            if ((ig.input.mouse.x > this.pos.x && ig.input.mouse.x < (this.pos.x + this.size.x)) &&
                (ig.input.mouse.y > this.pos.y && ig.input.mouse.y < (this.pos.y + this.size.y)))
            {
                // if we have hitmask defined then check if we inside or outside of hitmask,
                // if no hitmask then we already inside and return true
                if (this.hitmask) {
                    return this.hitmask.hittest(ig.input.mouse.x - this.pos.x, ig.input.mouse.y - this.pos.y, 0 /* we only interested in frame 0 so no need to pass any other*/);
                } else {
                    return true;
                }
            }
            return false;                       // hittest failed - mouse outside of entity
        },
        init: function(x, y, settings) {

            this.animSheet = new ig.AnimationSheet('media/btns/' + settings.image , settings.width, settings.height);
            this.addAnim('btnNormal', 1, [0]);
            this.addAnim('btnHover', 2, [1]);
            this.addAnim('btnDown', 3, [2]);
            this.addAnim('btnDisabled', 4, [3]);
            this.currentAnim = this.anims.btnNormal;
            this.size.x = settings.width;
            this.size.y = settings.height;

            // ...
            // ...
            this.btnStateMachine = new ig.ButtonStateMachine();
            this.btnStateMachine.isMouseInside = this.hittest.bind(this);
            this.btnStateMachine.isMouseDown = function() { return ig.input.state('click'); };
            this.btnStateMachine.startClick = this.startClick.bind(this);
            this.btnStateMachine.endClick = this.endClick.bind(this);
            this.btnStateMachine.startHover = this.startHover.bind(this);
            this.btnStateMachine.endHover = this.endHover.bind(this);
            this.btnStateMachine.endClickIgnore = this.endClickIgnore.bind(this);

            this.hitmask = new ig.ScaledAlphaHitmask();
            this.hitmask.scale = 8;
            this.hitmask.verticalFrames = 4;                // normal | hover | down | disabled
            this.hitmask.drawHitmask = true;                // set to 'false' to disable drawing of hitmask over btn image
            this.hitmask.setImage(this.animSheet.image);

            this.parent(x, y, settings);
        },
        update: function() {
            this.parent();

            this.btnStateMachine.updateState();
        }
    });
});

1 decade ago by ShawnSwander

I really don't understand how to implement this or if this is what I want.

Can we make entities clickable only on the pixels that are opaque? If so, do we really need multiple plugins like this? Its very confusing to know whats going on here.

1 decade ago by Heiko

Here's a simpler example.
All that you need to add is a 32x32 pixel image called "redball_32.png", and it should work - or adjust the entity size to fit your test image size.


ig.module( 
	'game.main' 
)
.requires(
	'impact.game',
	'plugins.scaled-alpha-hitmask'
)
.defines(function(){

MyEntity = ig.Entity.extend({
		hitmask: null,
		image: null,
		size: {x: 32, y: 32},

		// hittest - first check if mouse in bounds of entity, 
		//         - then check if mouse hit visible part of image inside entity
        hittest: function() {
            if ((ig.input.mouse.x > this.pos.x && ig.input.mouse.x < (this.pos.x + this.size.x)) &&
                (ig.input.mouse.y > this.pos.y && ig.input.mouse.y < (this.pos.y + this.size.y)))
            {
                // if we have hitmask defined then check if we inside or outside of hitmask,
                // if no hitmask then we already inside and return true
                if (this.hitmask) {
                    return this.hitmask.hittest(ig.input.mouse.x - this.pos.x, ig.input.mouse.y - this.pos.y, 0 /* we only interested in frame 0 so no need to pass any other*/);
                } else {
                    return true;				// no hitmask, but hit was inside entity, so we still return true
                }
            }
            return false;                       // hittest failed - mouse outside of entity
        },

		init: function(x, y, settings) {
			this.image = new ig.Image('media/redball_32.png');

			this.hitmask = new ig.ScaledAlphaHitmask();
			this.hitmask.drawHitmask = false;		// don't draw debug mask over image
			this.hitmask.setImage(this.image);

			this.parent(x, y, settings);
		},
		update: function() {
			this.parent();
			if (ig.input.pressed('click')) {
				console.log('mouse clicked (could be anywhere on canvas)');
				if (this.hittest()) {
					console.log('mouse clicked inside entity !!! (and hit opaque pixel)');
				}
			}
		},

		draw: function() {
			this.parent();

			if (this.image)
				this.image.drawTile(this.pos.x, this.pos.y, 0, this.image.width);	
		}
});


MyGame = ig.Game.extend({
	myentity: null,
	
	init: function() {
		// Initialize your game here; bind keys etc.
		ig.input.bind(ig.KEY.MOUSE1, 'click');					// bind left mouse click

		this.myentity = this.spawnEntity(MyEntity, 100, 100); 	// create instance of MyEntity
	},
});


ig.main( '#canvas', MyGame, 60, 240, 240, 1 );

});

1 decade ago by Heiko

Note: Scaling of canvas (ig.system.scale) not yet supported, will try and get this working ASAP.

I should probably move the hittest function into the plugin, maybe that will simplify things a bit more.

@ShawnSwander - yes you can make entities clickable only on opaque pixels - that is the intent. The initial example is simplified code for my use case. I haven't used it for anything besides buttons yet - but as demonstrated in code above - should also work with simpler entities.

1 decade ago by ShawnSwander

I noticed the iamge in your code is hard coded in this line
 this.image.drawTile(this.pos.x, this.pos.y, 0, this.image.width);  

can you set this to the entities current animation frame without setting another property of the entity to pull it from?

1 decade ago by Heiko

I'm not sure what you are trying to do.

In the example above I just created a simple entity with a single image.

You can use something like the following if you want to use an animation instead:

MyEntity = ig.Entity.extend({
		hitmask: null,
		size: {x: 32, y: 32},

		// hittest - first check if mouse in bounds of entity, 
		//         - then check if mouse hit visible part of image inside entity
        hittest: function() {
            if ((ig.input.mouse.x > this.pos.x && ig.input.mouse.x < (this.pos.x + this.size.x)) &&
                (ig.input.mouse.y > this.pos.y && ig.input.mouse.y < (this.pos.y + this.size.y)))
            {
                // if we have hitmask defined then check if we inside or outside of hitmask,
                // if no hitmask then we already inside and return true
                if (this.hitmask) {
                    return this.hitmask.hittest(ig.input.mouse.x - this.pos.x, ig.input.mouse.y - this.pos.y, 0 /* we only interested in frame 0 so no need to pass any other*/);
                } else {
                    return true;				// no hitmask, but hit was inside entity, so we still return true
                }
            }
            return false;                       // hittest failed - mouse outside of entity
        },

		init: function(x, y, settings) {
        	this.animSheet: new ig.AnimationSheet( 'media/yourAnimation.png', 32, 32 ),		
			// Add the animations
			this.addAnim( 'frame1', 1, [0] );
			this.addAnim( 'frame2', 1, [1] );
			this.currentAnim = this.anims.frame1;

			this.hitmask = new ig.ScaledAlphaHitmask();
			// this.hitmask.drawHitmask = false;		// don't draw debug mask over image
			this.hitmask.setImage(this.animSheet.image);

	  
			this.parent(x, y, settings);
		},
		update: function() {
			this.parent();
			if (ig.input.pressed('click')) {
				console.log('mouse clicked');
				if (this.hittest()) {
					console.log('mouse clicked inside entity (and hit opaque pixel)');
				}
			}
		},

		draw: function() {
			this.parent();

			// this.currentAnim will get drawn by the base class (Entity)
		}
});


NOTE: that the alpha hitmask will by default be created from the first frame. And that it currently only supports vertical frames (i.e. where frame2 is below frame1 in the image). Currently no support for horizontal frames, or a combination of vertical and horizontal frames, although possible with a bit more work.

1 decade ago by Heiko

Updated plugin to now support ig.system.scale.
Added entityHittest function to plugin.
Added new minimal example (as outlined above).

1 decade ago by ShawnSwander

I think this plugin is a bit heavy on resources would drawing the hitmask with smaller image blown up be lest heavy on resources? perhalf half the size?

1 decade ago by Heiko

If I understand you correctly, that functionality is already supported.

The following line will reduce the size of the (in memory) hitmask (pixel array) by 8.
this.hitmask.scale = 8;

.H

1 decade ago by Heiko

If you set

this.hitmask.drawHitmask = true;

it will create a full size debug image of the hitmask created - but this is only meant for debugging purposes to 'visualize' the mask that has been created.

In release this value should be set to false.

1 decade ago by ShawnSwander

That's what I thought scale did but I wasn't sure. Scale is helpful but it is still heavy on resources. is there a way to preload the hitmasks?

1 decade ago by Heiko

Hi Shawn.

The current version doesn't support preloading. But I just coded a mockup and with a few lines of extra code it can be modified to support preloading.

I can either mail you the modified code or post it to github - just haven't done thorough testing though and would need to update my documentation and examples a bit.

Basically hooked into the image onload function to load hitmask before image pre-loading completes.

Needed to add extra code to ScaledAlphaHitmask init code.

1 decade ago by Heiko

Ok uploaded latest changes to github.

Updated scaled-alpha-hitmask.js plugin and added index3.html to demo code to test pre-loading.

.H
Page 1 of 1
« first « previous next › last »