How to draw a segment of a circle in Cocos2d-x? - c++

Context
I try to draw pie chart for statistic in my game. I'm using Cocos2d-x ver.3.8.1. Size of the game is important, so I won't to use third-party frameworks to create pie charts.
Problem
I could not find any suitable method in Cocos2d-x for drawing part of the circle.
I tried to do
I tried to find a solution to this problem in Internet, but without success.
As is known, sector of a circle = triangle + segment. So, I tried to use the method drawSegment() from DrawNode also.
Although it has parameter radius ("The segment radius" written in API reference), radius affects only the thickness of the line.
drawSegment() method draw a simple line, the thickness of which is set by a method call.
Question
Please prompt me, how can I draw a segment or a sector of a circle in Cocos2d-x?
Any advice will be appreciated, thanks.

I think the one of the ways to draw a sector of a circle in Cocos2d-X is the way to use drawPolygon on DrawNode. I wrote little sample.
void drawSector(cocos2d::DrawNode* node, cocos2d::Vec2 origin, float radius, float angle_degree,
cocos2d::Color4F fillColor, float borderWidth, cocos2d::Color4F bordercolor,
unsigned int num_of_points = 100)
{
if (!node)
{
return;
}
const cocos2d::Vec2 start = origin + cocos2d::Vec2{radius, 0};
const auto angle_step = 2 * M_PI * angle_degree / 360.f / num_of_points;
std::vector<cocos2d::Point> circle;
circle.emplace_back(origin);
for (int i = 0; i <= num_of_points; i++)
{
auto rads = angle_step * i;
auto x = origin.x + radius * cosf(rads);
auto y = origin.y + radius * sinf(rads);
circle.emplace_back(x, y);
}
node->drawPolygon(circle.data(), circle.size(), fillColor, borderWidth, bordercolor);
}
This is the function to calculate the position of edge point of circle and draw polygon. If you want to use it, you need to call like following,
auto canvas = DrawNode::create();
drawSector(canvas, cocos2d::Vec2(400, 400), 100, 60, cocos2d::Color4F::GREEN, 2, cocos2d::Color4F::BLUE, 100);
this->addChild(triangle);
The result would be like this. I think the code will help your problem.

Related

Drawing a sprite on the circumference of a circle based on the position of other objects

I'm making a sniper shooter arcade style game in Gamemaker Studio 2 and I want the position of targets outside of the viewport to be pointed to by chevrons that move along the circumference of the scope when it moves. I am using trig techniques to determine the coordinates but the chevron is jumping around and doesn't seem to be pointing to the target. I have the code broken into two: the code to determine the coordinates in the step event of the enemies class (the objects that will be pointed to) and a draw event in the same class. Additionally, when I try to rotate the chevron so it also points to the enemy, it doesn't draw at all.
Here's the coordinate algorithm and the code to draw the chevrons, respectively
//determine the angle the target makes with the player
delta_x = abs(ObjectPlayer.x - x); //x axis displacement
delta_y = abs(ObjectPlayer.y - y); //y axis displacement
angle = arctan2(delta_y,delta_x); //angle in radians
angle *= 180/pi //angle in radians
//Determine the direction based on the larger dimension and
largest_distance = max(x,y);
plusOrMinus = (largest_distance == x)?
sign(ObjectPlayer.x-x) : sign(ObjectPlayer.y-y);
//define the chevron coordinates
chevron_x = ObjectPlayer.x + plusOrMinus*(cos(angle) + 20);
chevron_y = ObjectPlayer.y + plusOrMinus*(sign(angle) + 20);
The drawing code
if(object_exists(ObjectEnemy)){
draw_text(ObjectPlayer.x, ObjectPlayer.y-10,string(angle));
draw_sprite(Spr_Chevron,-1,chevron_x,chevron_y);
//sSpr_Chevron.image_angle = angle;
}
Your current code is slightly more complex that it needs to be for this, if you want to draw chevrons pointing towards all enemies, you might as well do that on spot in Draw. And use degree-based functions if you're going to need degrees for drawing anyway
var px = ObjectPlayer.x;
var py = ObjectPlayer.y;
with (ObjectEnemy) {
var angle = point_direction(px, py, x, y);
var chevron_x = px + lengthdir_x(20, angle);
var chevron_y = py + lengthdir_y(20, angle);
draw_sprite_ext(Spr_Chevron, -1, chevron_x, chevron_y, 1, 1, angle, c_white, 1);
}
(also see: an almost-decade old blog post of mine about doing this while clamping to screen edges instead)
Specific problems with your existing code are:
Using a single-axis plusOrMinus with two axes
Adding 20 to sine/cosine instead of multiplying them by it
Trying to apply an angle to sSpr_Chevron (?) instead of using draw_sprite_ext to draw a rotated sprite.
Calculating largest_distance based on executing instance's X/Y instead of delta X/Y.

Projecting line from camera

I'm trying to convert a viewport click onto a world position for an object.
It would be quite simple if all I wanted was to draw a point exactly where the user clicks in the canvas:
void Canvas::getClickPosition(int x, int y, Vector3d(&out)[2]) const
{
Vector4d point4d[2];
Vector2d point2d(x, y);
int w = canvas.width();
int h = canvas.height();
Matrix4d model = m_world * m_camera;
for (int i = 0; i < 2; ++i) {
Vector4d sw(point2d.x() / (0.5 * w) - 1,
point2d.y() / (0.5* h) - 1, i * 1, 1);
point4d[i] = (m_proj * model).inverse() * sw;
out[i] = point4d.block<1, 3>(0, 0);
}
}
The expected behavior is achieved with this simple code.
The problem arises when I try to actually make a line that will look like a one pixel when the user first clicks it. Until the camera is rotated in any direction it should look like it was perfectly shot from the camera and that it has whatever length (doesn't matter).
I tried the obvious:
Vector4d sw(point2d.x() / (0.5 * w) - 1,
point2d.y() / (0.5* h) - 1, 1, 1); // Z is now 1 instead of 0.
The result is, as most of you guys should expect, a line that pursues the vanishing point, at the center of the screen. Therefore, the farther I click from the center, the more the line is twitched from it's expected direction.
What can I do to have a line show as a dot from the click point of view, no matter where at the screen?
EDIT: for better clarity, I'm trying to draw the lines like this:
glBegin(GL_LINES);
line.p1 = m_proj * (m_world * m_camera) * line.p1;
line.p2 = m_proj * (m_world * m_camera) * line.p2;
glVertex3f(line.p1.x(), line.p1.y(), line.p1.z());
glVertex3f(line.p2.x(), line.p2.y(), line.p2.z());
glEnd();
Your initial attempt is actually very close. The only thing you are missing is the perspective divide:
out[i] = point4d.block<1, 3>(0, 0) / point4d.w();
Depending on your projection matrix, you might also need to specify a z-value of -1 for the near plane instead of 0.
And yes, your order of matrices projection * model * view seems strange. But as long as you keep the same order in both procedures, you should get a consistent result.
Make sure that the y-axis of your window coordinate system is pointing upwards. Otherwise, you will get a result that is reflected at the horizontal midline.

Why does QPainter::drawPoint draw a horizontal line segment?

I'm trying to draw a 3-pixel large point with QPainter. But the following code instead draws a horizontal line with width of 3 pixels.
#include <QPainter>
#include <QImage>
int main()
{
const int w=1000, h=1000;
QImage img(w, h, QImage::Format_RGBX8888);
{
QPainter p(&img);
p.fillRect(0,0,w,h,Qt::black);
p.scale(w,h);
p.setPen(QPen(Qt::red, 3./w, Qt::SolidLine, Qt::RoundCap));
p.drawPoint(QPointF(0.1,0.1));
}
img.save("test.png");
}
Here's the top left corner of the resulting image:
I am expecting to get a point which is red circle, or at least a square — but instead I get this line segment. If I comment out p.scale(w,h) and draw the point with width 3 (instead of 3./w) at position (100,100), then I get a small mostly symmetric 3-pixel in height and width point.
What's going on? Why do I get a line segment instead of point as expected? And how to fix it, without resorting to drawing an ellipse or to avoiding QPainter::scale?
I'm using Qt 5.10.0 on Linux x86 with g++ 5.5.0. The same happens on Qt 5.5.1.
It appears that QPaintEngineEx::drawPoints renders points as line segments of length 1/63.. See the following code from qtbase/src/gui/painting/qpaintengineex.cpp in the Qt sources:
void QPaintEngineEx::drawPoints(const QPointF *points, int pointCount)
{
QPen pen = state()->pen;
if (pen.capStyle() == Qt::FlatCap)
pen.setCapStyle(Qt::SquareCap);
if (pen.brush().isOpaque()) {
while (pointCount > 0) {
int count = qMin(pointCount, 16);
qreal pts[64];
int oset = -1;
for (int i=0; i<count; ++i) {
pts[++oset] = points[i].x();
pts[++oset] = points[i].y();
pts[++oset] = points[i].x() + 1/63.;
pts[++oset] = points[i].y();
}
QVectorPath path(pts, count * 2, qpaintengineex_line_types_16, QVectorPath::LinesHint);
stroke(path, pen);
pointCount -= 16;
points += 16;
}
} else {
for (int i=0; i<pointCount; ++i) {
qreal pts[] = { points[i].x(), points[i].y(), points[i].x() + qreal(1/63.), points[i].y() };
QVectorPath path(pts, 2, 0);
stroke(path, pen);
}
}
}
Notice the pts[++oset] = points[i].x() + 1/63.; line in the opaque brush branch. This is the second vertex of the path — shifted with respect to the desired position of the point.
This explains why the line extends to the right of the position requested and why it depends on the scale. So, it seems the code in the OP isn't wrong for an ideal QPainter implementation, but just has come across a Qt bug (be it in the implementation of the method or in its documentation).
So the conclusion: one has to work around this problem by either using different scale, or drawing ellipses, or drawing line segments with much smaller lengths than what QPainter::drawPoints does.
I've reported this as QTBUG-70409.
Though I have not been able to pin point why exactly the problem occurs, I have got relatively close to the solution. The problem lies with scaling. I did a lots of trial and error with different scaling and width of point with the below code.
const int w=500, h=500;
const int scale = 100;
float xPos = 250;
float yPos = 250;
float widthF = 5;
QImage img(w, h, QImage::Format_RGBX8888);
{
QPainter p(&img);
p.setRenderHints(QPainter::Antialiasing);
p.fillRect(0,0,w,h,Qt::black);
p.scale(scale, scale);
p.setPen(QPen(Qt::red, widthF/(scale), Qt::SolidLine, Qt::RoundCap));
p.drawPoint(QPointF(xPos/scale, yPos/scale));
}
img.save("test.png");
The above code produces the image
My observations are
1) Due to high scaling, the point (which is just 3 pixel wide) is not able to scale proportionally at lower width (width of point), if you set width as something like 30 the round shape is visible.
2) If you want to keep the width of point low then you have to decrease the scaling.
Sadly I can not explain why at high scaling it is not expanding proportionally.

Fanning out an "arc" of card meshes

I have n number of cards. Each card is a units in width.
Many popular card games display a hand of cards in the "fanned out" position (see images below), and I would like to do the same. By utilizing the following formula, I'm able to place cards in an arc:
// NOTE: UE4 uses a left-handed, Z-up coordinate system.
// (+X = Forward, +Y = Right, and +Z = Up)
// NOTE: Card meshes have their pivot points in the center of the mesh
// (meshSize * 0.5f = local origin of mesh)
// n = Number of card meshes
// a = Width of each card mesh
const auto arcWidth = 0.8f;
const auto arcHeight = 0.15f;
const auto rotationAngle = 30.f;
const auto deltaAngle = 180.f;
const auto delta = FMath::DegreesToRadians(deltaAngle) / (float)(n);
const auto halfDelta = delta * 0.5f;
const auto halfMeshWidth = a * 0.5f;
const auto radius = halfMeshWidth + (rotationAngle / FMath::Tan(halfDelta));
for (unsigned y = 0; y < n; y++)
{
auto ArcX = (radius * arcWidth) * FMath::Cos(((float)y * delta) + halfDelta);
auto ArcY = (radius * arcHeight) * FMath::Sin(((float)y * delta) + halfDelta);
auto ArcVector = FVector(0.f, ArcX, ArcY);
// Draw a line from the world origin to the card origin
DrawDebugLine(GetWorld(), FVector::ZeroVector, ArcVector, FColor::Magenta, true, -1.f, 0, 2.5f);
}
Here's a 5-Card example from Hearthstone:
Here's a 5-Card example from Slay The Spire:
But the results I'm producing are, well... Suboptimal:
No matter how I tweak the variables, the cards on the far left and far right side are getting squashed together into the hand. I imagine this has to do with how the points of a circle are distributed, and then squashed downwards (via arcHeight) to form an ellipse? In any case, you can see that the results are far from similar, even though if you look closely at the example references, you can see that an arc exists from the center of each card (before those cards are rotated in local space).
What can I do to achieve a more evenly spaced arc?
Your distribution does look like an ellipse. What you need is a very large circle, where the center of the circle is way off the bottom of the screen. Something like the circle below, where the black rectangle is the screen area where you're drawing the cards, and the green dots are the card locations. Note that the radius of the circle is large, and the angles between the cards are small.

Box2D collision detection failure

I have recently began using Box2D version 2.1 in combination with Allegro5. Currently, I built a test with a ground and 4 boxes. 3 boxes are stacked, and the other one smashes causing them to flip. During this demonstration, I noticed got two glitches.
One is that creating a box in Box2D "SetAsBox( width, height )", only gives half the size of a normal box drawn to the screen using allegro. Example: In Box2D, I create a box the size of (15, 15). When I come to draw the shape using allegro, I must make an offset of -15 on the y, and scale the shape twice its size.
The other issue is during the collision detection while my boxes rotate due to impact. Most squares hit the ground, but some of them have an offset from the ground of its height making it floating.
Here is the code for making my boxes:
cBox2D::cBox2D( int width, int height ) {
// Note: In Box2D, 30 pixels = 1 meter
velocityIterations = 10;
positionIterations = 10;
worldGravity = 9.81f;
timeStep = ( 1.0f / 60.0f );
isBodySleep = false;
gravity.Set( 0.0f, worldGravity );
world = new b2World( gravity, isBodySleep );
groundBodyDef.position.Set( 0.0f, height ); // ground location
groundBody = world->CreateBody( &groundBodyDef );
groundBox.SetAsBox( width, 0.0f ); // Ground size
groundBody->CreateFixture( &groundBox, 0.0f );
}
cBox2D::~cBox2D( void ) {}
void cBox2D::makeSquare( int width, int height, int locX, int locY, float xVelocity, float yVelocity, float angle, float angleVelocity ) {
sSquare square;
square.bodyDef.type = b2_dynamicBody;
square.bodyDef.position.Set( locX, locY ); // Box location
square.bodyDef.angle = angle; // Box angle
square.bodyDef.angularVelocity = angleVelocity;
square.bodyDef.linearVelocity.Set( xVelocity, yVelocity ); // Box Velocity
square.body = world->CreateBody( &square.bodyDef );
square.dynamicBox.SetAsBox( width, height ); // Box size
square.fixtureDef.shape = &square.dynamicBox;
square.fixtureDef.density = 1.0f;
square.fixtureDef.friction = 0.3f;
square.fixtureDef.restitution = 0.0f; // Bouncyness
square.body->CreateFixture( &square.fixtureDef );
squareVec.push_back( square );
}
int cBox2D::getVecSize( void ) {
return squareVec.size();
}
b2Body* cBox2D::getSquareAt( int loc ) {
return squareVec.at( loc ).body;
}
void cBox2D::update( void ) {
world->Step(timeStep, velocityIterations, positionIterations);
world->ClearForces();
}
Edit:
Thank you Chris Burt-Brown for explaining the first issue to me, as for the second issue, It was a good idea, but it did not solve it. I tried both rounding methods you showed me.
Edit:
I think I found the answer to my second issue. Turns out that Allegro has a different coordinate system than OpenGL. As a result, instead of doing -gravity, I had to do +gravity which caused Box2D to become unstable and behave weird.
Edit:
My bad, I thought it was the issue, but turns out it did not change a thing.
It's actually SetAsBox(halfwidth, halfheight). I know it sounds weird but take a look inside SetAsBox. Passing in the parameters 15 and 15 will give a box with corners (-15,-15) and (15,15) i.e. a box of size 30x30.
I think it's intended as an optimisation, but it's a pretty silly one.
I don't know what's causing your other problem, but when you draw the boxes with Allegro, try seeing if it's fixed when you round the coordinates. (If that doesn't work, try ceil.)