Adding Widget hover detection to the Interactor
From C4 Engine Wiki
Contents |
Overview
This tutorial explains how to add basic hover detection to the Interactor, which will enable you to test whether any given Widget is currently under it within the HandleInteractionEvent function. I'm also including a very basic example of how to use this to create a mouseover effect on a button. You will be modifying several source files (MGGame.h, MGGame.cpp, MGFighter.h and MGFighter.cpp) by copying and pasting the code provided. This tutorial was recently updated to provide easier access to the current panel and widgets being hovered from anywhere in your game.
You can view the result of this tutorial here
Limitations
This technique will work effectively for Panels of any size, aspect or rotation, viewed from any angle, for rectangular Widgets of any size or aspect. If you need support for non-rectangular shapes or for rotated Widgets you will need to implement the additional functionality yourself.
The example below assumes you are adding this to the FighterInteractor class which is included with Mangler (the official demo game), since most people reading this will likely have their Interactor functionality based on that.
Official support by Terathon for similar functionality is planned for version 2.2 - this is meant as a temporary solution only. While this method does not require any modifications to the Engine itself, it will likely be rendered obsolete in the future.
Steps
1. You must give a name (called a Widget identifier in the Panel Editor) to any Widget you want to detect. The first thing you should do is open C4 2.0 and load the Tutorial/world/Lasers.wld. Once you've done that, find the Panel shown in figure 1-1 and open the Panel Editor for it. For this example we will create a simple mouseover effect for the button that says "Click Me". This button is already named "BTTN" so we'll go with that - otherwise you would have to give one to the object yourself.
figure 1-1
2. We will need two colors for our button mouseover. We will display one normally, and the other when the Interactor is hovering over it. In the editor, select the green button (not the text on top of it) so that you can view it's properties on the right as shown in figure 2-1. Now click the color to open the color picker and you will see 4 numbers here which determine this color. You will need to these numbers later, so write them down for each of the 2 colors you choose. For this tutorial, I'm using a semi-transparent green for the normal state (0, 255, 0, 64), and the standard green which is used in the Lasers.wld already as the mouseover color (0, 255, 0, 255). Once you've chosen your colors, leave it on the color you want to use for the "normal" state and then save this world as "Tutorial/worlds/Lasers2.wld". Close C4, you're done with it for the moment.
figure 2-1
3. Open MGGame.h and add this to the private section of the Game class definition:
List<Widget> *hoverWidgets; PanelController *hoverPanel;
and this to the public section:
List<Widget> * GetHoverWidgets(void)
{
return hoverWidgets;
}
void SetHoverWidgets(List<Widget> *widgets)
{
hoverWidgets = widgets;
}
PanelController * GetHoverPanel(void)
{
return hoverPanel;
}
void SetHoverPanel(PanelController *widgetPanelController)
{
hoverPanel = widgetPanelController;
}
4. Open MGGame.cpp, add this to the constructor:
hoverWidgets = new List<Widget>; hoverPanel = nullptr;
5. Open MGFighter.h, add this with the other includes:
#include "C4Panels.h"
then add this under the private section of your FighterInteractor class definition:
// next 1 line is necessary for the example button only Widget *myTestButton;
and this under public:
List<Widget> * GetInteractionHovers(Node *node, const Point3D *position = nullptr);
6. Open MGFighter.cpp and add this to the FighterInteractor constructor (the function which has fighterController = controller; in it).
// next 1 line is necessary for the example button only myTestButton = nullptr;
then paste this before the HandleInteractionEvent function (the line starting with void FighterInteractor::HandleInteractionEvent):
Please notice the addition of TheGame->SetHoverPanel(effectController); since the previous version of this tutorial:
List<Widget> * FighterInteractor::GetInteractionHovers(Node *node, const Point3D *position)
{
// create a list to hold the results
List<Widget> *result = new List<Widget>;
// get the node controller
Controller *controller = node->GetController();
if (controller)
{
// make sure the node is an effect
if (node->GetNodeType() == kNodeEffect)
{
// get a pointer to the panel
const Effect *effect = static_cast<Effect *>(node);
if (effect->GetEffectType() == kEffectPanel)
{
// get the panel controller
PanelController *effectController = static_cast<PanelController *>(effect->GetController());
// store a pointer to the current panel
TheGame->SetHoverPanel(effectController);
// get the root widget of the panel
Widget *rootWidget = static_cast<PanelController *>(effectController)->GetRootWidget();
// get the internal size of the panel
Vector3D effectSize = effectController->GetInternalPanelSize();
// get the horizontal and vertical scale of the panel
const Point3D *effectVertex = effect->GetAttributeArray<Point3D>(kArrayVertex);
Point3D effectScale = Point3D(position->x / effectVertex[2].x, position->y / effectVertex[2].y, 0.0F);
// determine the interactor's local position on the panel
Point3D mop = Point3D(effectScale.x * effectSize.x, effectSize.y - ( effectScale.y * effectSize.y ), 0.0F);
// iterate through the widgets
Widget *widget = rootWidget->GetRightmostNode();
while (widget != rootWidget)
{
// make sure the panel is visible and enabled
unsigned long state = widget->GetWidgetState();
if ((state & (kWidgetDisabled | kWidgetHidden)) == 0)
{
// get the local position of the widget
Vector3D wip = widget->GetWidgetPosition();
// get the local size of the widget
Vector3D wis = widget->GetWidgetSize();
// check whether the interactor's position is within the bounding rect of the widget
// note: C4 version 2.1 will have native hover detection.
if( (mop.x > wip.x) && (mop.y > wip.y) && (mop.x < (wip.x + wis.x)) && (mop.y < (wip.y + wis.y)))
{
// add a pointer to this widget to the results
result->Append(widget);
}
}
// check the next widget on the panel
widget = rootWidget->GetPreviousNode(widget);
}
}
}
}
return result;
}
7. Within the FighterInteractor::HandleInteractionEvent function, paste this underneath the line that says Interactor::HandleInteractionEvent(type, node, position);:
// these color values represent strength of the RGB and alpha channels.
// a value of 1.0F is full strength, which is 255 in the Panel Editor.
// Figure out your values from the colors your chose earlier.
// next 3 lines are necessary for the example button only
ColorRGBA colorNormal = ColorRGBA(0.0F, 1.0F, 0.0F, 0.25F);
ColorRGBA colorOver = ColorRGBA(0.0F, 1.0F, 0.0F, 1.0F);
bool isOverMyButton = false;
if(type == kInteractionEventTrack)
{
// replace the current list of widgets with an updated one
TheGame->SetHoverWidgets(GetInteractionHovers(node, position));
// the rest of this block is necessary for the example button only
if (TheGame->GetHoverWidgets()->GetElementCount() > 0)
{
Widget *widget = TheGame->GetHoverWidgets()->First();
while (widget)
{
if (Text::CompareText(Text::TypeToString(widget->GetWidgetKey()), "BTTN"))
{
// the Interactor is currently over the widget named BTTN
myTestButton = widget;
myTestButton->SetWidgetColor( colorOver );
isOverMyButton = true;
}
widget = widget->ListElement<Widget>::Next();
}
}
if (myTestButton)
{
myTestButton->SetWidgetColor( isOverMyButton ? colorOver : colorNormal );
}
}
and paste this underneath the line that says "case kInteractionEventDisengage:":
TheGame->SetHoverWidgets(new List<Widget>);
TheGame->SetHoverPanel(nullptr);
// the rest is necessary for the example button only
if (myTestButton)
{
myTestButton->SetWidgetColor( colorNormal );
}
You should now be able to rebuild and run your game, play the Tutorial/worlds/Lasers2.wld and have a mouseover effect on the button as shown in the video.
Other Notes
You can test whether the Interactor is over any specific panel by comparing node->GetNodeName() of course, and you can add as many hover tests as you want along with the one for "BTTN" shown above. However, this example is only meant to show you how to accomplish a Widget mouseover, it's obviously not good practice to mash up all your code in the same class - but it was convenient for demonstration purposes.
If your kInteractionEventEngage and kInteractionEventDisengage events are not being called at the precise edge of your Panels (which is a requirement for "hover" detection to function properly), you may need to change the kCameraPositionHeight constant being used with the SetInteractionProbe position from a value of 1.6F to 1.5F. This problem may have been a result of my own adjustments, but I'm mentioning it just in case someone else has the same issue.
If you're creating a game which makes use of this technique, you'll want to consider writing your own class which inherits from panels and manages it's own Widget's hover states, rather that making a mess of your FighterInteractor class and the HandleInteractionEvent.
Hopefully, what's demonstrated here is enough to give anyone looking to spice up their Panels with some mouseovers the incentive to do it.


