2 // Copyright (C) 2006 Matthias Braun <matze@braunis.de>
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with this program. If not, see <http://www.gnu.org/licenses/>.
17 #include "object/camera.hpp"
22 #include "util/reader.hpp"
23 #include "util/writer.hpp"
24 #include "lisp/parser.hpp"
25 #include "object/path_walker.hpp"
26 #include "object/player.hpp"
27 #include "scripting/camera.hpp"
28 #include "scripting/squirrel_util.hpp"
29 #include "supertux/globals.hpp"
30 #include "supertux/sector.hpp"
32 /* this is the fractional distance toward the peek
33 position to move each frame; lower is slower,
34 0 is never get there, 1 is instant */
35 static const float PEEK_ARRIVE_RATIO = 0.1;
40 // 0 = No, 1 = Fix, 2 = Mario/Yoshi, 3 = Kirby, 4 = Super Metroid-like
44 float kirby_rectsize_x;
45 float kirby_rectsize_y;
46 // where to fix the player (used for Yoshi and Fix camera)
49 // maximum scrolling speed in Y direction
52 // factor to dynamically increase max_speed_x based on player speed
53 float dynamic_max_speed_x;
55 // time the player has to face into the other direction before we assume a
60 // when too change from noscroll mode back to lookahead left/right mode
61 // set to <= 0 to disable noscroll mode
67 float dynamic_speed_sm;
72 kirby_rectsize_x(0.2f),
73 kirby_rectsize_y(0.34f),
78 dynamic_max_speed_x(1.0),
84 dynamic_speed_sm(0.8f)
88 void load(const std::string& filename)
91 const lisp::Lisp* root = parser.parse(filename);
92 const lisp::Lisp* camconfig = root->get_lisp("camera-config");
94 throw std::runtime_error("file is not a camera config file.");
96 camconfig->get("xmode", xmode);
97 camconfig->get("ymode", ymode);
98 camconfig->get("target-x", target_x);
99 camconfig->get("target-y", target_y);
100 camconfig->get("max-speed-x", max_speed_x);
101 camconfig->get("max-speed-y", max_speed_y);
102 camconfig->get("dynamic-max-speed-x", dynamic_max_speed_x);
103 camconfig->get("dirchange-time", dirchange_time);
104 camconfig->get("clamp-x", clamp_x);
105 camconfig->get("clamp-y", clamp_y);
106 camconfig->get("kirby-rectsize-x", kirby_rectsize_x);
107 camconfig->get("kirby-rectsize-y", kirby_rectsize_y);
108 camconfig->get("edge-x", edge_x);
109 camconfig->get("sensitive-x", sensitive_x);
110 camconfig->get("dynamic-speed-sm", dynamic_speed_sm);
114 Camera::Camera(Sector* newsector, std::string name_) :
118 lookahead_mode(LOOKAHEAD_NONE),
122 cached_translation(),
136 config = new CameraConfig();
146 Camera::expose(HSQUIRRELVM vm, SQInteger table_idx)
148 if(name.empty()) return;
149 scripting::Camera* _this = new scripting::Camera(this);
150 expose_object(vm, table_idx, _this, name, true);
154 Camera::unexpose(HSQUIRRELVM vm, SQInteger table_idx)
156 if(name.empty()) return;
157 scripting::unexpose_object(vm, table_idx, name);
161 Camera::draw(DrawingContext& )
166 Camera::get_translation() const
172 Camera::parse(const Reader& reader)
174 std::string modename;
176 reader.get("mode", modename);
177 if(modename == "normal") {
179 } else if(modename == "autoscroll") {
182 const lisp::Lisp* pathLisp = reader.get_lisp("path");
184 throw std::runtime_error("No path specified in autoscroll camera.");
186 autoscroll_path.reset(new Path());
187 autoscroll_path->read(*pathLisp);
188 autoscroll_walker.reset(new PathWalker(autoscroll_path.get()));
189 } else if(modename == "manual") {
192 std::stringstream str;
193 str << "invalid camera mode '" << modename << "'found in worldfile.";
194 throw std::runtime_error(str.str());
199 Camera::reset(const Vector& tuxpos)
201 translation.x = tuxpos.x - SCREEN_WIDTH/2;
202 translation.y = tuxpos.y - SCREEN_HEIGHT/2;
206 keep_in_bounds(translation);
208 cached_translation = translation;
212 Camera::shake(float time, float x, float y)
214 shaketimer.start(time);
217 shakespeed = M_PI/2 / time;
221 Camera::scroll_to(const Vector& goal, float scrolltime)
223 scroll_from = translation;
225 keep_in_bounds(scroll_goal);
228 scrollspeed = 1.0 / scrolltime;
232 static const float CAMERA_EPSILON = .00001f;
233 static const float MAX_SPEED_Y = 140;
236 Camera::update(float elapsed_time)
240 update_scroll_normal(elapsed_time);
243 update_scroll_autoscroll(elapsed_time);
246 update_scroll_to(elapsed_time);
255 Camera::reload_config()
257 if(PHYSFS_exists("camera.cfg")) {
259 config->load("camera.cfg");
260 log_info << "Loaded camera.cfg." << std::endl;
261 } catch(std::exception &e) {
262 log_debug << "Couldn't load camera.cfg, using defaults ("
263 << e.what() << ")" << std::endl;
268 float clamp(float val, float min, float max)
279 Camera::keep_in_bounds(Vector& translation_)
281 float width = sector->get_width();
282 float height = sector->get_height();
284 // don't scroll before the start or after the level's end
285 translation_.x = clamp(translation_.x, 0, width - SCREEN_WIDTH);
286 translation_.y = clamp(translation_.y, 0, height - SCREEN_HEIGHT);
288 if (height < SCREEN_HEIGHT)
289 translation_.y = height/2.0 - SCREEN_HEIGHT/2.0;
290 if (width < SCREEN_WIDTH)
291 translation_.x = width/2.0 - SCREEN_WIDTH/2.0;
297 if(shaketimer.started()) {
298 translation.x -= sin(shaketimer.get_timegone() * shakespeed) * shakedepth_x;
299 translation.y -= sin(shaketimer.get_timegone() * shakespeed) * shakedepth_y;
304 Camera::update_scroll_normal(float elapsed_time)
306 const CameraConfig& config_ = *(this->config);
307 Player* player = sector->player;
308 // TODO: co-op mode needs a good camera
309 Vector player_pos(player->get_bbox().get_middle().x,
310 player->get_bbox().get_bottom());
311 static Vector last_player_pos = player_pos;
312 Vector player_delta = player_pos - last_player_pos;
313 last_player_pos = player_pos;
315 // check that we don't have division by zero later
316 if(elapsed_time < CAMERA_EPSILON)
319 /****** Vertical Scrolling part ******/
320 int ymode = config_.ymode;
322 if(player->is_dying() || sector->get_height() == 19*32) {
326 cached_translation.y = player_pos.y - SCREEN_HEIGHT * config_.target_y;
329 // target_y is the high we target our scrolling at. This is not always the
330 // high of the player, but if he is jumping upwards we should use the
331 // position where he last touched the ground. (this probably needs
332 // exceptions for trampolines and similar things in the future)
334 if(player->fall_mode == Player::JUMPING)
335 target_y = player->last_ground_y + player->get_bbox().get_height();
337 target_y = player->get_bbox().p2.y;
338 target_y -= SCREEN_HEIGHT * config_.target_y;
340 // delta_y is the distance we'd have to travel to directly reach target_y
341 float delta_y = cached_translation.y - target_y;
342 // speed is the speed the camera would need to reach target_y in this frame
343 float speed_y = delta_y / elapsed_time;
345 // limit the camera speed when jumping upwards
346 if(player->fall_mode != Player::FALLING
347 && player->fall_mode != Player::TRAMPOLINE_JUMP) {
348 speed_y = clamp(speed_y, -config_.max_speed_y, config_.max_speed_y);
351 // scroll with calculated speed
352 cached_translation.y -= speed_y * elapsed_time;
355 float halfsize = config_.kirby_rectsize_y * 0.5f;
356 cached_translation.y = clamp(cached_translation.y,
357 player_pos.y - SCREEN_HEIGHT * (0.5f + halfsize),
358 player_pos.y - SCREEN_HEIGHT * (0.5f - halfsize));
361 float upperend = SCREEN_HEIGHT * config_.edge_x;
362 float lowerend = SCREEN_HEIGHT * (1 - config_.edge_x);
364 if (player_delta.y < -CAMERA_EPSILON) {
366 lookahead_pos.y -= player_delta.y * config_.dynamic_speed_sm;
368 if(lookahead_pos.y > lowerend) {
369 lookahead_pos.y = lowerend;
371 } else if (player_delta.y > CAMERA_EPSILON) {
373 lookahead_pos.y -= player_delta.y * config_.dynamic_speed_sm;
374 if(lookahead_pos.y < upperend) {
375 lookahead_pos.y = upperend;
379 // adjust for level ends
380 if (player_pos.y < upperend) {
381 lookahead_pos.y = upperend;
383 if (player_pos.y > sector->get_width() - upperend) {
384 lookahead_pos.y = lowerend;
387 cached_translation.y = player_pos.y - lookahead_pos.y;
390 translation.y = cached_translation.y;
393 float top_edge, bottom_edge;
394 if(config_.clamp_y <= 0) {
396 bottom_edge = SCREEN_HEIGHT;
398 top_edge = SCREEN_HEIGHT*config_.clamp_y;
399 bottom_edge = SCREEN_HEIGHT*(1-config_.clamp_y);
403 float translation_compensation = player_pos.y - translation.y;
405 if(player->peeking_direction_y() == ::UP) {
406 peek_to = bottom_edge - translation_compensation;
407 } else if(player->peeking_direction_y() == ::DOWN) {
408 peek_to = top_edge - translation_compensation;
411 float peek_move = (peek_to - peek_pos.y) * PEEK_ARRIVE_RATIO;
412 if(fabs(peek_move) < 1.0) {
416 peek_pos.y += peek_move;
418 translation.y -= peek_pos.y;
420 if(config_.clamp_y > 0) {
421 translation.y = clamp(translation.y,
422 player_pos.y - SCREEN_HEIGHT * (1-config_.clamp_y),
423 player_pos.y - SCREEN_HEIGHT * config_.clamp_y);
424 cached_translation.y = clamp(cached_translation.y,
425 player_pos.y - SCREEN_HEIGHT * (1-config_.clamp_y),
426 player_pos.y - SCREEN_HEIGHT * config_.clamp_y);
430 /****** Horizontal scrolling part *******/
431 int xmode = config_.xmode;
433 if(player->is_dying())
437 cached_translation.x = player_pos.x - SCREEN_WIDTH * config_.target_x;
440 // our camera is either in leftscrolling, rightscrolling or
443 // when suddenly changing directions while scrolling into the other
444 // direction abort scrolling, since tux might be going left/right at a
445 // relatively small part of the map (like when jumping upwards)
447 // Find out direction in which the player moves
448 LookaheadMode walkDirection;
449 if (player_delta.x < -CAMERA_EPSILON) walkDirection = LOOKAHEAD_LEFT;
450 else if (player_delta.x > CAMERA_EPSILON) walkDirection = LOOKAHEAD_RIGHT;
451 else if (player->dir == ::LEFT) walkDirection = LOOKAHEAD_LEFT;
452 else walkDirection = LOOKAHEAD_RIGHT;
454 float LEFTEND, RIGHTEND;
455 if(config_.sensitive_x > 0) {
456 LEFTEND = SCREEN_WIDTH * config_.sensitive_x;
457 RIGHTEND = SCREEN_WIDTH * (1-config_.sensitive_x);
459 LEFTEND = SCREEN_WIDTH;
463 if(lookahead_mode == LOOKAHEAD_NONE) {
464 /* if we're undecided then look if we crossed the left or right
465 * "sensitive" area */
466 if(player_pos.x < cached_translation.x + LEFTEND) {
467 lookahead_mode = LOOKAHEAD_LEFT;
468 } else if(player_pos.x > cached_translation.x + RIGHTEND) {
469 lookahead_mode = LOOKAHEAD_RIGHT;
471 /* at the ends of a level it's obvious which way we will go */
472 if(player_pos.x < SCREEN_WIDTH*0.5) {
473 lookahead_mode = LOOKAHEAD_RIGHT;
474 } else if(player_pos.x >= sector->get_width() - SCREEN_WIDTH*0.5) {
475 lookahead_mode = LOOKAHEAD_LEFT;
479 } else if(lookahead_mode != walkDirection) {
480 /* player changed direction while camera was scrolling...
481 * he has to do this for a certain time to add robustness against
484 changetime = game_time;
485 } else if(game_time - changetime > config_.dirchange_time) {
486 if(lookahead_mode == LOOKAHEAD_LEFT &&
487 player_pos.x > cached_translation.x + RIGHTEND) {
488 lookahead_mode = LOOKAHEAD_RIGHT;
489 } else if(lookahead_mode == LOOKAHEAD_RIGHT &&
490 player_pos.x < cached_translation.x + LEFTEND) {
491 lookahead_mode = LOOKAHEAD_LEFT;
493 lookahead_mode = LOOKAHEAD_NONE;
500 LEFTEND = SCREEN_WIDTH * config_.edge_x;
501 RIGHTEND = SCREEN_WIDTH * (1-config_.edge_x);
503 // calculate our scroll target depending on scroll mode
505 if(lookahead_mode == LOOKAHEAD_LEFT)
506 target_x = player_pos.x - RIGHTEND;
507 else if(lookahead_mode == LOOKAHEAD_RIGHT)
508 target_x = player_pos.x - LEFTEND;
510 target_x = cached_translation.x;
512 // that's the distance we would have to travel to reach target_x
513 float delta_x = cached_translation.x - target_x;
514 // the speed we'd need to travel to reach target_x in this frame
515 float speed_x = delta_x / elapsed_time;
518 float player_speed_x = player_delta.x / elapsed_time;
519 float maxv = config_.max_speed_x + (fabsf(player_speed_x * config_.dynamic_max_speed_x));
520 speed_x = clamp(speed_x, -maxv, maxv);
523 cached_translation.x -= speed_x * elapsed_time;
526 float halfsize = config_.kirby_rectsize_x * 0.5f;
527 cached_translation.x = clamp(cached_translation.x,
528 player_pos.x - SCREEN_WIDTH * (0.5f + halfsize),
529 player_pos.x - SCREEN_WIDTH * (0.5f - halfsize));
532 float LEFTEND = SCREEN_WIDTH * config_.edge_x;
533 float RIGHTEND = SCREEN_WIDTH * (1 - config_.edge_x);
535 if (player_delta.x < -CAMERA_EPSILON) {
537 lookahead_pos.x -= player_delta.x * config_.dynamic_speed_sm;
538 if(lookahead_pos.x > RIGHTEND) {
539 lookahead_pos.x = RIGHTEND;
542 } else if (player_delta.x > CAMERA_EPSILON) {
544 lookahead_pos.x -= player_delta.x * config_.dynamic_speed_sm;
545 if(lookahead_pos.x < LEFTEND) {
546 lookahead_pos.x = LEFTEND;
550 // adjust for level ends
551 if (player_pos.x < LEFTEND) {
552 lookahead_pos.x = LEFTEND;
554 if (player_pos.x > sector->get_width() - LEFTEND) {
555 lookahead_pos.x = RIGHTEND;
558 cached_translation.x = player_pos.x - lookahead_pos.x;
561 translation.x = cached_translation.x;
564 float left_edge, right_edge;
565 if(config_.clamp_x <= 0) {
567 right_edge = SCREEN_WIDTH;
569 left_edge = SCREEN_WIDTH*config_.clamp_x;
570 right_edge = SCREEN_WIDTH*(1-config_.clamp_x);
574 float translation_compensation = player_pos.x - translation.x;
576 if(player->peeking_direction_x() == ::LEFT) {
577 peek_to = right_edge - translation_compensation;
578 } else if(player->peeking_direction_x() == ::RIGHT) {
579 peek_to = left_edge - translation_compensation;
582 float peek_move = (peek_to - peek_pos.x) * PEEK_ARRIVE_RATIO;
583 if(fabs(peek_move) < 1.0) {
587 peek_pos.x += peek_move;
589 translation.x -= peek_pos.x;
591 if(config_.clamp_x > 0) {
592 translation.x = clamp(translation.x,
593 player_pos.x - SCREEN_WIDTH * (1-config_.clamp_x),
594 player_pos.x - SCREEN_WIDTH * config_.clamp_x);
596 cached_translation.x = clamp(cached_translation.x,
597 player_pos.x - SCREEN_WIDTH * (1-config_.clamp_x),
598 player_pos.x - SCREEN_WIDTH * config_.clamp_x);
602 keep_in_bounds(translation);
603 keep_in_bounds(cached_translation);
607 Camera::update_scroll_autoscroll(float elapsed_time)
609 Player* player = sector->player;
610 if(player->is_dying())
613 translation = autoscroll_walker->advance(elapsed_time);
615 keep_in_bounds(translation);
619 Camera::update_scroll_to(float elapsed_time)
621 scroll_to_pos += elapsed_time * scrollspeed;
622 if(scroll_to_pos >= 1.0) {
624 translation = scroll_goal;
628 translation = scroll_from + (scroll_goal - scroll_from) * scroll_to_pos;
632 Camera::get_center() const {
633 return translation + Vector(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);