📜 ⬆️ ⬇️

Approximation of the curve in the boom trajectory for the game St.Val

In this post I will tell you how to create a control in a mobile application using drawing a trajectory. This control is used in Harbor Master and FlightControl: a player draws a finger along the line along which ships and airplanes move. For my game, St.Val needed a similar mechanic. How I did it and what I had to face - read below.



A few words about the game. In St.Val, the main goal is to connect hearts by color with arrows. The task of the player: to build the trajectory of the flight of the arrow so that it connects the hearts in flight. The game was created based on Cocos2D 2.1 for iOS, below the video game mechanics.
')


Main tasks


To create a control, you need to solve three problems:
  1. Read coordinates
  2. Smooth and approximate them
  3. Launch arrow on them


Plus, separately, I will describe the loop detection algorithm in the trajectories that I needed to expand the game mechanics.

Under the cut the solution of these problems and a link to the demonstration project.



The code for the demo project is available here: github.com/AndreyZarembo/TouchInput

How are the coordinates read?


Reading the coordinates of a finger is a simple task, because in Cocos2D there is work with individual Touch events divided by type. To receive them, the object implements the CCTouchOneByOneDelegate protocol and is registered with the Touch-Event Manager:
[[[CCDirector sharedDirector] touchDispatcher] addTargetedDelegate:self priority:0 swallowsTouches: YES]; 

The CCTouchOneByOneDelegate protocol includes methods:
 //    - (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event //     - (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event //   - (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event //  -     -   - (void)ccTouchCancelled:(UITouch *)touch withEvent:(UIEvent *)event 


The game requires only one finger, so it is enough to save the UITouch to the currentTouch variable at the first touch. If it is not nil, then the movement is already tracked.

When the finger is released, reset the currentTouch variable, and in the motion handler ccTouchMoved, check to see if this is the object being monitored. If yes, points are recorded.

Reef 1

All this works great, until the game's folding gestures are used and the control center panel does not pop up. In these cases, ccTouchCancelled is not triggered, but the ccTouchMoved event is no longer coming. You can fix this by checking the phase at your fingertips. If _currentTouch.phase == UITouchPhaseCancelled , then the finger should be changed:
 - (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event { if (currentTouch == nil || currentTouch.phase == UITouchPhaseCancelled) { currentTouch = touch; } return YES; } - (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event { if (touch == currentTouch) { // Save point } } - (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event { if (touch == currentTouch) { // End trajectory } } - (void)ccTouchCancelled:(UITouch *)touch withEvent:(UIEvent *)event { if (touch == currentTouch) { // End trajectory } } 


What to do with coordinates


The coordinates will have to be filtered and approximated so that the line looks smooth and objects move along it evenly.

For smoothing the curve, the filter by distance is used: all points must be at least 20px apart. This is one and a half times less than the finger on the screen, so filtering is hidden. When the filtering distance is 20px, the number of processed points decreases by 50-70%, in the limit it is 95% when the finger moves across the screen pixel by pixel.

The resulting chain of points must be approximated by a curve, using the Catmull-Roma spline for this. It passes through the specified 4 points, smoothes the steps and is simple to calculate.



To start the curve from the first point, we add the boundary conditions: the points are added in a straight line to the first and last segments. Then for N points we get N-1 segment.



The post turned out to be voluminous, so I will not talk in detail about the curve itself, below will be the code for calculating its segments.

Reef 2

In the curve described, the movement in screen coordinates will be uneven. In order to smooth the movement, each segment is divided into straight sections of 10px. This size was chosen for two reasons:
  1. this is a round number, so it is easy to determine how many segments are needed to place an object on a curve with a given normal coordinate (the distance traveled along the curve);
  2. this is a small enough size so that the gradation does not make itself felt, while the number of splitting points is reduced by an order of magnitude.


The mechanics of partitioning is quite simple. For each segment in the cycle, the points are searched in such a way as to cover a distance of 1px, each point is compared with the last saved point of the spline. If the distance is more than 10px, it is calculated how much longer it is, a correction is made along a straight line and the new point is added to the spline array. For optimization, this operation is performed only for new points. As a result, we obtain an array of points that are 10px apart from each other and repeat the trajectory of the finger.

You cannot draw an infinite trajectory in the game, so the condition of the end of drawing along the length was added.

The movement of objects


In the game, the trajectory is displayed by moving points ("tracks"). They are located on the curve every 20px and move uniformly towards the end of the trajectory. To create the effect of movement and simplify the animation, the points move within two segments of 10 pixels each, from 0 to 20, then return to 0 again. Due to the synchronous movement, they seem to move continuously from beginning to end.

If there are N + 1 points in the curve, then N segments along which the tracks move, respectively, N / 2 tracks should be placed. For all points, the offset T is set, within [0,2], which is used to calculate the coordinates of each of the tracks.

When T is 0 to 1, the position is calculated as
 Pt = Pt0*t+(1-t)*Pt1 

When T is from 1 to 2 position is calculated as
 Pt = Pt1*(t-1)+(2-t)*Pt2 




As a result, all points move "in single file".

Launch boom


The launch of the boom is done using Actions from Cocos 2D. It consists of the following steps:
  1. Setting the initial position of the boom
  2. Sequential movement and rotation of the boom along curve segments
  3. Hiding boom


There are more of these stages in the game, but the essence does not change.

To collect the sequence of actions and start their execution, all actions are sequentially added to NSMutableArray and passed to the CCSequence object to start the chain of actions.

The first is added CCCallBlock to set the initial position - these are the coordinates of the first point of the curve. Here, the arrow is set to full opacity.
 CCCallBlock *setInitialPosition = [CCCallBlock actionWithBlock:^{ _arrow.position = pointVal.CGPointValue; _arrow.opacity = 255; }]; [moves addObject: setInitialPosition]; 

Then all points of the trajectory are added successively, for the correct orientation the previous point is saved. The rotation of the arrow is determined from the difference of the coordinates of the current and past points using the arctangent.

Reef 3

Curve elements are obtained by almost 10 pixels, but not exactly, therefore, for a uniform boom movement, you need to specify the segment length and determine the time of movement along each segment based on the boom speed.
 CGPoint point = pointVal.CGPointValue; CGPoint prevPoint = prevPointVal.CGPointValue; CGPoint diff = CGPointMake(point.x-prevPoint.x, point.y-prevPoint.y); CGFloat distance = hypotf(diff.x,diff.y); CGFloat duration = distance / arrowSpeed; lastDirectionVector = CGPointMake(diff.x/distance, diff.y/distance); CGFloat angle = -atan2f(diff.y,diff.x)*180./M_PI; CCMoveTo *moveArrow = [CCMoveTo actionWithDuration: duration position: point]; CCRotateTo *rotateArrow = [CCRotateTo actionWithDuration: duration angle: angle]; CCSpawn *moveAndRotate = [CCSpawn actionWithArray: @[ moveArrow, rotateArrow ]]; [moves addObject: moveAndRotate]; 


To complete the flight, the arrow must fly a little further trajectory. For this, the lastDirectionVector variable stores the direction of the last segment in the form of a normalized vector. The arrow hides during hideEffectDuration , during which it flies in a straight line. To set the direction, the normalized direction vector is multiplied scalarly by the speed of the boom and by the time of disappearance.
 CCFadeTo *hideArrow = [CCFadeTo actionWithDuration: hideEffectDuration opacity:0]; CCMoveBy *moveArrow = [CCMoveBy actionWithDuration: hideEffectDuration position: CGPointMake(lastDirectionVector.x*arrowSpeed*hideEffectDuration, lastDirectionVector.y*arrowSpeed*hideEffectDuration)]; CCSpawn *moveAndHide = [CCSpawn actionWithArray: @[ moveArrow, hideArrow ]]; [moves addObject: moveAndHide]; 

After adding all the elements of the arrow is sent to the flight.
 [_arrow runAction: [CCSequence actionWithArray: moves]]; 


Loop detection


In one of the levels of the game, the hearts are united not by the trajectory of the arrow, but by looping around a pair of hearts (see video from 0:55). To implement this mechanic, you need to find the intersection of the trajectory with itself.

For this, the set of segments is viewed sequentially and it is checked whether the segment of the segment intersects with the segment of the previous segments. The intersection is determined using the "Oriented Triangle Area" method, since the intersection point itself is not important, and the numbers of intersecting segments are known from the cycle. The algorithm is taken from here:
e-maxx.ru/algo/segments_intersection_checking

Reef 4

The algorithm works well, but on a long curve slowly. Therefore, the check was modified to check not every segment of five, but one big one. The number five is magical and was chosen empirically. The starting point of the block of five points is taken, the first four are skipped, and the fifth is taken as the end, it will be the next starting point. Accuracy is reduced, but losses are acceptable. You can improve accuracy by checking small segments inside overlapping large ones.



All the loops found are stored in the array as the numbers of the initial and final segments of the ring. From them were obtained the points of the polygon UIBezierPath , which has the standard means of determining whether a point falls into it.

 [path containsPoint: position] 


That's all!

The code for the demo project is available here: github.com/AndreyZarembo/TouchInput

ps In the process of preparing the post, the code was slightly modified and optimized.

Source: https://habr.com/ru/post/219333/


All Articles