1 /******************************************************************************
2 * Warmux is a convivial mass murder game.
3 * Copyright (C) 2001-2011 Warmux Team.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
18 ******************************************************************************
19 * Camera : follow an object, center on it or follow mouse interaction.
20 *****************************************************************************/
21
22 #include <WARMUX_debug.h>
23 #include <WARMUX_random.h>
24
25 #include "character/character.h"
26 #include "game/config.h"
27 #include "game/game.h"
28 #include "game/game_time.h"
29 #include "graphic/video.h"
30 #include "include/app.h"
31 #include "interface/cursor.h"
32 #include "interface/interface.h"
33 #include "interface/mouse.h"
34 #include "map/camera.h"
35 #include "map/map.h"
36 #include "object/physical_obj.h"
37 #include "team/teams_list.h"
38 #include "tool/math_tools.h"
39 #include "tool/string_tools.h"
40
41 static const Point2f MAX_CAMERA_SPEED(5000, 5000);
42 static const Point2f MAX_CAMERA_ACCELERATION(1.5,1.5);
43
44 #define REACTIVITY 0.6f
45 #define SPEED_REACTIVITY 0.05f
46 #define REALTIME_FOLLOW_FACTOR 0.15f
47
48 #define ANTICIPATION 18
49 #define ADVANCE_ANTICIPATION 20
50 #define SPEED_REACTIVITY_CEIL 4
51 #define SCROLL_KEYBOARD 20 // pixel
52 #define REALTIME_FOLLOW_LIMIT 25
53 #define MAX_REFRESHES_PER_SECOND 100
54
Camera()55 Camera::Camera()
56 : m_started_shaking(0)
57 , m_shake_duration(0)
58 , m_shake_amplitude(0, 0)
59 , m_shake_centerpoint(0, 0)
60 , m_shake(0, 0)
61 , m_last_time_shake_calculated(0)
62 , m_speed(0, 0)
63 , m_stop(false)
64 , m_control_mode(NO_CAMERA_CONTROL)
65 , m_begin_controlled_move_time(0)
66 , m_mouse_counter(0)
67 , m_scroll_start_pos(0, 0)
68 , m_last_mouse_pos(0, 0)
69 , m_scroll_vector(0.0f, 0.0f)
70 , auto_crop(true)
71 , followed_object(NULL)
72 {
73 pointer_used_before_scroll = Mouse::POINTER_SELECT;
74 }
75
Reset()76 void Camera::Reset()
77 {
78 m_stop = false;
79 auto_crop = true;
80 followed_object = NULL;
81 m_begin_controlled_move_time = 0;
82 m_control_mode = NO_CAMERA_CONTROL;
83 SetXYabs(GetWorld().GetSize() / 2);
84 }
85
HasFixedX() const86 bool Camera::HasFixedX() const
87 {
88 return GetWorld().GetWidth() <= GetSizeX();
89 }
90
HasFixedY() const91 bool Camera::HasFixedY() const
92 {
93 return GetWorld().GetHeight() <= GetSizeY();
94 }
95
SetXYabs(int x,int y)96 void Camera::SetXYabs(int x, int y)
97 {
98 Surface &window = GetMainWindow();
99
100 if (!HasFixedX())
101 position.x = InRange_Long(x, 0, GetWorld().GetWidth() - GetSizeX());
102 else
103 position.x = (GetWorld().GetWidth() - window.GetWidth())/2;
104
105 if (!HasFixedY())
106 position.y = InRange_Long(y, 0, GetWorld().GetHeight() - GetSizeY());
107 else
108 position.y = (GetWorld().GetHeight() - window.GetHeight())/2;
109 }
110
SetXY(Point2i pos)111 void Camera::SetXY(Point2i pos)
112 {
113 pos = pos*FreeDegrees();
114 if (pos.IsNull())
115 return;
116
117 SetXYabs(position + pos);
118 }
119
AutoCrop()120 void Camera::AutoCrop()
121 {
122 /* Stuff is put static in order to be able to reach the last position
123 * of the object the camera was following, in case it disappears. This
124 * typically happen when something explodes or a character dies. */
125 static Point2i obj_pos(0, 0);
126
127 Point2i target(0,0);
128 bool stop = false;
129
130 //Stop kinetic scrolling from interfering camera movement
131 m_scroll_vector = Point2f();
132
133 if (followed_object && !followed_object->IsGhost()) {
134
135 /* compute the ideal position!
136 * it takes the physical object direction into account
137 */
138 obj_pos = followed_object->GetCenter();
139
140 if (obj_pos > GetPosition() + (GetSize()>>3) &&
141 obj_pos < GetPosition() + ((7 * GetSize())>>3)) {
142 if (m_stop)
143 stop = true;
144
145 } else {
146 m_stop = false;
147 }
148
149 target = obj_pos;
150
151 if (followed_object->IsMoving()) {
152 float fps = Game::GetInstance()->GetLastFrameRate();
153 if (fps < 0.1f)
154 fps = 0.1f;
155 Double time_delta = 1000 / (Double)fps;
156 Point2d anticipation = followed_object->GetSpeed() * time_delta;
157
158 //limit anticipation to screen size/3
159 Point2d anticipation_limit = GetSize()/3;
160 target += anticipation.clamp(-anticipation_limit, anticipation_limit);
161 }
162
163 target -= GetSize()>>1;
164
165 } else {
166 target = GetPosition();
167 m_stop = true;
168 }
169
170 //Compute new speed to reach target
171 Point2f acceleration = (target - m_speed*ANTICIPATION - position)*REACTIVITY;
172 // Limit acceleration
173 acceleration = acceleration.clamp(-MAX_CAMERA_ACCELERATION, MAX_CAMERA_ACCELERATION);
174
175 // std::cout<<"acceleration before : "<<acceleration.x<<" "<<acceleration.y<<std::endl;
176 if ((int)abs(m_speed.x) > SPEED_REACTIVITY_CEIL) {
177 acceleration.x *= (1 + SPEED_REACTIVITY * ((int)abs(m_speed.x) - SPEED_REACTIVITY_CEIL));
178 }
179
180 if ((int)abs(m_speed.y) > SPEED_REACTIVITY_CEIL) {
181 acceleration.y *= (1 + SPEED_REACTIVITY * ((int)abs(m_speed.y) - SPEED_REACTIVITY_CEIL));
182 }
183
184 //printf("speed=(%.2f,%.2f) target=(%i,%i)\n", m_speed.x, m_speed.y, target.x, target.y);
185 if (stop) {
186 m_speed = m_speed/2;
187
188 } else {
189
190 //Apply acceleration
191 m_speed += acceleration;
192
193 // Realtime follow is enabled if object is too fast to be correctly followed
194 // stop can only be true if followed_obj!=NULL
195 // speed is set to 0 if not moving
196 if (abs((int)followed_object->GetSpeed().x) > REALTIME_FOLLOW_LIMIT) {
197 m_speed.x = (target.x - position.x) * REALTIME_FOLLOW_FACTOR;
198 }
199
200 if (abs((int)followed_object->GetSpeed().y) > REALTIME_FOLLOW_LIMIT) {
201 m_speed.y = (target.y - position.y) * REALTIME_FOLLOW_FACTOR;
202 }
203
204 //Limit
205 m_speed = m_speed.clamp(-MAX_CAMERA_SPEED, MAX_CAMERA_SPEED);
206 }
207
208 //Update position
209 Point2i next_position((int)m_speed.x, (int)m_speed.y);
210 //printf("pos=(%i,%i) speed=(%.2f,%.2f)\n", next_position.x, next_position.y, m_speed.x, m_speed.y);
211 SetXY(next_position);
212
213 if (!m_stop && next_position.IsNull() && followed_object->GetSpeed().IsNull()) {
214 m_stop = true;
215 }
216 }
217
SaveMouseCursor()218 void Camera::SaveMouseCursor()
219 {
220 Mouse::pointer_t current_pointer = Mouse::GetInstance()->GetPointer();
221 if (current_pointer != Mouse::POINTER_MOVE &&
222 current_pointer != Mouse::POINTER_ARROW_UP &&
223 current_pointer != Mouse::POINTER_ARROW_DOWN &&
224 current_pointer != Mouse::POINTER_ARROW_LEFT &&
225 current_pointer != Mouse::POINTER_ARROW_RIGHT &&
226 current_pointer != Mouse::POINTER_ARROW_DOWN_RIGHT &&
227 current_pointer != Mouse::POINTER_ARROW_UP_RIGHT &&
228 current_pointer != Mouse::POINTER_ARROW_UP_LEFT &&
229 current_pointer != Mouse::POINTER_ARROW_DOWN_LEFT) {
230 pointer_used_before_scroll = current_pointer;
231 }
232 }
233
RestoreMouseCursor()234 void Camera::RestoreMouseCursor()
235 {
236 Mouse::GetInstance()->SetPointer(pointer_used_before_scroll);
237 }
238
ScrollCamera()239 void Camera::ScrollCamera()
240 {
241 if (!Mouse::GetInstance()->HasFocus()) // The application has not the focus, don't move the camera!!
242 return;
243
244 Point2i mousePos = Mouse::GetInstance()->GetPosition();
245 bool over_interface = Interface::GetInstance()->Intersect(mousePos);
246
247 if (!Config::GetInstance()->GetScrollOnBorder()) {
248 /* Kinetic scrolling */
249 if (!SDL_GetMouseState(NULL, NULL) || over_interface) {
250 m_scroll_start_pos = Point2i();
251 m_last_mouse_pos = Point2i();
252 m_mouse_counter = 0;
253
254 if (!m_scroll_vector.IsNull()) {
255 Point2f brk = 0.2f * m_scroll_vector.GetfloatNormal();
256
257 MSG_DEBUG("camera",
258 "scroll_vector=(%.3f,%.3f) scroll_vector_break=(%.3f,%.3f)\n",
259 m_scroll_vector.GetX(), m_scroll_vector.GetY(),
260 brk.GetX(), brk.GetY());
261
262 m_scroll_vector -= brk;
263 SetXY(-m_scroll_vector);
264 if (m_scroll_vector.Norm() < 1)
265 m_scroll_vector = Point2f();
266 }
267 return;
268 }
269
270 if (over_interface)
271 return;
272
273 m_mouse_counter++;
274
275 if (m_scroll_start_pos.IsNull())
276 m_scroll_start_pos = mousePos;
277 if (m_last_mouse_pos.IsNull())
278 m_last_mouse_pos = mousePos;
279
280 m_scroll_vector = mousePos - m_scroll_start_pos;
281 m_scroll_vector = m_scroll_vector / m_mouse_counter;
282 MSG_DEBUG("camera",
283 "scroll_vector=(%.3f,%.3f) mousePos=(%i,%i) lastMousePos=(%i,%i) scrollStartPos=(%i,%i)",
284 m_scroll_vector.GetX(), m_scroll_vector.GetY(),
285 mousePos.GetX(), mousePos.GetY(),
286 m_last_mouse_pos.GetX(), m_last_mouse_pos.GetY(),
287 m_scroll_start_pos.GetX(), m_scroll_start_pos.GetY());
288
289 if (mousePos != m_last_mouse_pos) {
290 SetXY(-(mousePos-m_last_mouse_pos));
291 m_last_mouse_pos = mousePos;
292 SetAutoCrop(false);
293 }
294 } else {
295
296 uint zone_size = Config::GetInstance()->GetScrollBorderSize();
297 Point2i sensitZone(zone_size, zone_size);
298
299 /* tstVector represents the vector of how deep the cursor is in a sensit
300 * zone; negative value means that the camera has to reduce its coordinates,
301 * a positive value means that it should increase. Actually reduce means
302 * LEFT/UP (for x/y) and increase RIGHT/DOWN directions.
303 * The bigger tstVector is, the faster the camera will scroll. */
304 Point2i tstVector;
305 tstVector = GetSize().inf(mousePos + sensitZone) * (mousePos + sensitZone - GetSize()) ;
306 tstVector -= mousePos.inf(sensitZone) * (sensitZone - mousePos);
307
308 if (!tstVector.IsNull()) {
309 SetXY(tstVector);
310 SetAutoCrop(false);
311 }
312
313 /* mouse pointer ***********************************************************/
314 SaveMouseCursor();
315
316 if (tstVector.IsNull())
317 RestoreMouseCursor();
318 else if (tstVector.IsXNull() && tstVector.y < 0)
319 Mouse::GetInstance()->SetPointer(Mouse::POINTER_ARROW_UP);
320 else if (tstVector.IsXNull() && tstVector.y > 0)
321 Mouse::GetInstance()->SetPointer(Mouse::POINTER_ARROW_DOWN);
322 else if (tstVector.IsYNull() && tstVector.x < 0)
323 Mouse::GetInstance()->SetPointer(Mouse::POINTER_ARROW_LEFT);
324 else if (tstVector.IsYNull() && tstVector.x > 0)
325 Mouse::GetInstance()->SetPointer(Mouse::POINTER_ARROW_RIGHT);
326 else if (tstVector.y > 0 && tstVector.x > 0)
327 Mouse::GetInstance()->SetPointer(Mouse::POINTER_ARROW_DOWN_RIGHT);
328 else if (tstVector.y < 0 && tstVector.x > 0)
329 Mouse::GetInstance()->SetPointer(Mouse::POINTER_ARROW_UP_RIGHT);
330 else if (tstVector.y < 0 && tstVector.x < 0)
331 Mouse::GetInstance()->SetPointer(Mouse::POINTER_ARROW_UP_LEFT);
332 else if (tstVector.y > 0 && tstVector.x < 0)
333 Mouse::GetInstance()->SetPointer(Mouse::POINTER_ARROW_DOWN_LEFT);
334 /***************************************************************************/
335 }
336 }
337
HandleMouseMovement()338 void Camera::HandleMouseMovement()
339 {
340 static Point2i first_mouse_pos(-1, -1);
341 static Point2i last_mouse_pos(0, 0);
342 Point2i curr_pos = Mouse::GetInstance()->GetPosition();
343
344 int x,y;
345 //Move camera with mouse holding Ctrl key down or with middle button of mouse
346 if (SDL_GetMouseState(&x, &y) & SDL_BUTTON(SDL_BUTTON_MIDDLE)
347 || SDL_GetModState() & KMOD_CTRL) {
348
349 // Begin to move the camera...
350 if (Mouse::GetInstance()->GetPointer() != Mouse::POINTER_MOVE) {
351 first_mouse_pos = Point2i(x, y);
352 SaveMouseCursor();
353 Mouse::GetInstance()->SetPointer(Mouse::POINTER_MOVE);
354 }
355
356 SetAutoCrop(false);
357 SetXY(last_mouse_pos - curr_pos);
358 last_mouse_pos = curr_pos;
359
360 if (m_begin_controlled_move_time == 0) {
361 m_begin_controlled_move_time = GameTime::GetInstance()->Read();
362 }
363
364 if (SDL_GetModState() & KMOD_CTRL) {
365 m_control_mode = KEYBOARD_CAMERA_CONTROL;
366 } else {
367 m_control_mode = MOUSE_CAMERA_CONTROL;
368 }
369 return;
370
371 } else if (m_control_mode == MOUSE_CAMERA_CONTROL) {
372
373 // if the mouse has not moved at all since the user pressed the middle button, we center the camera!
374 if (abs((int)first_mouse_pos.x - curr_pos.x) < 5 &&
375 abs((int)first_mouse_pos.y - curr_pos.y) < 5 &&
376 GameTime::GetInstance()->Read() - m_begin_controlled_move_time < 500) {
377 CenterOnActiveCharacter();
378 }
379
380 first_mouse_pos = Point2i(-1, -1);
381 RestoreMouseCursor();
382 m_control_mode = NO_CAMERA_CONTROL;
383 m_begin_controlled_move_time = 0;
384
385 } else if (m_control_mode == KEYBOARD_CAMERA_CONTROL) {
386 first_mouse_pos = Point2i(-1, -1);
387 RestoreMouseCursor();
388 m_control_mode = NO_CAMERA_CONTROL;
389 m_begin_controlled_move_time = 0;
390 }
391
392 ScrollCamera();
393 last_mouse_pos = curr_pos;
394 }
395
HandleMoveIntentions()396 void Camera::HandleMoveIntentions()
397 {
398 const UDMoveIntention * ud_move_intention = GetLastUDMoveIntention();
399 if (ud_move_intention) {
400 if (ud_move_intention->GetDirection() == DIRECTION_UP)
401 SetXY(Point2i(0, -SCROLL_KEYBOARD));
402 else
403 SetXY(Point2i(0, SCROLL_KEYBOARD));
404 }
405 const LRMoveIntention * lr_move_intention = GetLastLRMoveIntention();
406 if (lr_move_intention) {
407 if (lr_move_intention->GetDirection() == DIRECTION_RIGHT)
408 SetXY(Point2i(SCROLL_KEYBOARD, 0));
409 else
410 SetXY(Point2i(-SCROLL_KEYBOARD, 0));
411 }
412 if (lr_move_intention || ud_move_intention)
413 SetAutoCrop(false);
414 }
415
Refresh(bool ignore_user)416 void Camera::Refresh(bool ignore_user)
417 {
418 // Refresh gets called very often when the game is paused.
419 // This "if" ensures that the camera doesn't move too fast.
420 if (refresh_stopwatch.GetValue() >= 1000 / MAX_REFRESHES_PER_SECOND) {
421 // Check if player wants the camera to move
422 if (!ignore_user) {
423 HandleMouseMovement();
424 HandleMoveIntentions();
425 }
426
427 if (auto_crop && followed_object)
428 AutoCrop();
429 refresh_stopwatch.Reset();
430 }
431 }
432
FollowObject(const PhysicalObj * obj,bool follow_closely)433 void Camera::FollowObject(const PhysicalObj *obj, bool follow_closely)
434 {
435 MSG_DEBUG("camera.tracking", "Following object %s (%d)", obj->GetName().c_str(), follow_closely);
436
437 Mouse::GetInstance()->Hide();
438
439 auto_crop = true;
440
441 m_stop = !follow_closely;
442 followed_object = obj;
443 }
444
StopFollowingObj(const PhysicalObj * obj)445 void Camera::StopFollowingObj(const PhysicalObj* obj)
446 {
447 if (followed_object == obj) {
448 followed_object = NULL;
449 m_stop = true;
450 m_speed = Point2f(0,0);
451 }
452 }
453
IsVisible(const PhysicalObj & obj) const454 bool Camera::IsVisible(const PhysicalObj &obj) const
455 {
456 return Intersect(obj.GetRect());
457 }
458
CenterOnActiveCharacter()459 void Camera::CenterOnActiveCharacter()
460 {
461 CharacterCursor::GetInstance()->FollowActiveCharacter();
462 FollowObject(&ActiveCharacter(),true);
463 }
464
ComputeShake() const465 Point2i Camera::ComputeShake() const
466 {
467 uint time = GameTime::GetInstance()->Read();
468 ASSERT(time >= m_started_shaking);
469
470 if (time > m_started_shaking + m_shake_duration || m_shake_duration == 0) {
471 return Point2i(0, 0); // not shaking now
472 }
473
474 if (time == m_last_time_shake_calculated)
475 return m_shake;
476
477 // FIXME: we can underflow to 0 if time and m_started_shaking are large enough
478 float t = (float)(time - m_started_shaking) / (float)m_shake_duration;
479
480 float func_val = 1.0f;
481 if (t >= 0.001f) {
482 float k_scale_angle = float(10 * M_PI);
483 float arg = k_scale_angle * t;
484 // denormalized sinc
485 func_val = (1 - t) * sin(arg) / arg;
486 }
487
488 float x_ampl = RandomLocal().Getfloat(-m_shake_amplitude.x, m_shake_amplitude.x);
489 float y_ampl = RandomLocal().Getfloat(-m_shake_amplitude.y, m_shake_amplitude.y);
490
491 m_shake.x = x_ampl * func_val + m_shake_centerpoint.x;
492 m_shake.y = y_ampl * func_val + m_shake_centerpoint.y;
493
494 static uint t_last_time_logged = 0;
495 if (time - t_last_time_logged > 10) {
496 MSG_DEBUG("camera.shake", "Shaking: time = %d, t = %.3f, func_val = %.3f, shake: %d, %d",
497 time, t, func_val, m_shake.x, m_shake.y);
498 t_last_time_logged = time;
499 }
500
501 m_last_time_shake_calculated = time;
502 return m_shake;
503 }
504
Shake(uint how_long_msec,const Point2i & amplitude,const Point2i & centerpoint)505 void Camera::Shake(uint how_long_msec, const Point2i & amplitude, const Point2i & centerpoint)
506 {
507 MSG_DEBUG("camera.shake", "Shake added!");
508
509 uint time = GameTime::GetInstance()->Read();
510
511 ASSERT(time >= m_started_shaking);
512
513 if (m_started_shaking + m_shake_duration > time) {
514 // still shaking, so add amplitude/centerpoint to allow shakes to combine
515 m_shake_amplitude = max(m_shake_amplitude, amplitude);
516 m_shake_centerpoint = centerpoint;
517
518 // increase shake duration so it lasts how_long_msec from this time
519 m_shake_duration = how_long_msec + (time - m_started_shaking);
520 } else {
521 // reinit the shake
522 m_started_shaking = time;
523 m_shake_duration = how_long_msec;
524 m_shake_amplitude = amplitude;
525 m_shake_centerpoint = centerpoint;
526 }
527 }
528
ResetShake()529 void Camera::ResetShake()
530 {
531 m_started_shaking = 0;
532 m_shake_duration = 0;
533 m_last_time_shake_calculated = 0;
534 m_shake = Point2i(0, 0);
535 }
536