1 /*
2 C-Dogs SDL
3 A port of the legendary (and fun) action/arcade cdogs.
4 Copyright (c) 2013-2019 Cong Xu
5
6 Redistribution and use in source and binary forms, with or without
7 modification, are permitted provided that the following conditions are met:
8
9 Redistributions of source code must retain the above copyright notice, this
10 list of conditions and the following disclaimer.
11 Redistributions in binary form must reproduce the above copyright notice,
12 this list of conditions and the following disclaimer in the documentation
13 and/or other materials provided with the distribution.
14
15 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25 POSSIBILITY OF SUCH DAMAGE.
26 */
27 #include "camera.h"
28
29 #include "actors.h"
30 #include "draw/draw.h"
31 #include "draw/drawtools.h"
32 #include "events.h"
33 #include "font.h"
34 #include "log.h"
35 #include "los.h"
36 #include "player.h"
37
38
39 #define PAN_SPEED 4
40
CameraInit(Camera * camera)41 void CameraInit(Camera *camera)
42 {
43 memset(camera, 0, sizeof *camera);
44 CameraReset(camera);
45 camera->lastPosition = svec2_zero();
46 HUDInit(&camera->HUD, &gGraphicsDevice, &gMission);
47 camera->shake = ScreenShakeZero();
48 }
49
CameraReset(Camera * camera)50 void CameraReset(Camera *camera)
51 {
52 DrawBufferTerminate(&camera->Buffer);
53 DrawBufferInit(
54 &camera->Buffer, svec2i(X_TILES, Y_TILES), &gGraphicsDevice);
55 }
56
CameraTerminate(Camera * camera)57 void CameraTerminate(Camera *camera)
58 {
59 DrawBufferTerminate(&camera->Buffer);
60 HUDTerminate(&camera->HUD);
61 }
62
CameraInput(Camera * camera,const int cmd,const int lastCmd)63 void CameraInput(Camera *camera, const int cmd, const int lastCmd)
64 {
65 // Control the camera
66 if (camera->spectateMode == SPECTATE_NONE)
67 {
68 return;
69 }
70 // Arrows: pan camera
71 // CMD1/2: choose next player to follow
72 if (CMD_HAS_DIRECTION(cmd))
73 {
74 camera->spectateMode = SPECTATE_FREE;
75 const int pan = PAN_SPEED;
76 if (cmd & CMD_LEFT) camera->lastPosition.x -= pan;
77 else if (cmd & CMD_RIGHT) camera->lastPosition.x += pan;
78 if (cmd & CMD_UP) camera->lastPosition.y -= pan;
79 else if (cmd & CMD_DOWN) camera->lastPosition.y += pan;
80 }
81 else if ((AnyButton(cmd) && !AnyButton(lastCmd)) ||
82 camera->FollowNextPlayer)
83 {
84 // Can't follow if there are no players
85 if (GetNumPlayers(PLAYER_ALIVE_OR_DYING, false, false) == 0)
86 {
87 return;
88 }
89 camera->spectateMode = SPECTATE_FOLLOW;
90 // Find index of player
91 int playerIndex = -1;
92 CA_FOREACH(const PlayerData, p, gPlayerDatas)
93 if (p->UID == camera->FollowActorUID)
94 {
95 playerIndex = _ca_index;
96 break;
97 }
98 CA_FOREACH_END()
99 // Get the next player by index that has an actor in the game
100 const int d = (cmd & CMD_BUTTON1) ? 1 : -1;
101 for (int i = playerIndex + d;; i += d)
102 {
103 i = CLAMP_OPPOSITE(i, 0, (int)gPlayerDatas.size - 1);
104 // Check if clamping made us hit the termination condition
105 if (i == playerIndex) break;
106 const PlayerData *p = CArrayGet(&gPlayerDatas, i);
107 if (IsPlayerAliveOrDying(p))
108 {
109 // Follow this player
110 camera->FollowActorUID = p->ActorUID;
111 camera->FollowNextPlayer = false;
112 break;
113 }
114 }
115 }
116 }
117
118 static struct vec2 GetFollowPlayerPos(
119 const struct vec2 lastPos, const PlayerData *p);
CameraUpdate(Camera * camera,const int ticks,const int ms)120 void CameraUpdate(Camera *camera, const int ticks, const int ms)
121 {
122 camera->HUD.DrawData = HUDGetDrawData();
123 if (camera->HUD.DrawData.NumScreens == 0)
124 {
125 // Try to spectate if there are other players alive
126 if (camera->spectateMode == SPECTATE_NONE)
127 {
128 // Enter spectator mode
129 // Free-look mode
130 camera->spectateMode = SPECTATE_FREE;
131 // If there are other players alive, follow them
132 CA_FOREACH(const PlayerData, p, gPlayerDatas)
133 if (IsPlayerAliveOrDying(p))
134 {
135 camera->spectateMode = SPECTATE_FOLLOW;
136 camera->FollowActorUID = p->ActorUID;
137 break;
138 }
139 CA_FOREACH_END()
140 }
141 }
142 else
143 {
144 // Don't spectate
145 camera->spectateMode = SPECTATE_NONE;
146 }
147 if (camera->spectateMode == SPECTATE_FOLLOW)
148 {
149 const TActor *a = ActorGetByUID(camera->FollowActorUID);
150 if (a != NULL)
151 {
152 camera->HUD.DrawData.Players[0] = PlayerDataGetByUID(a->PlayerUID);
153 if (camera->HUD.DrawData.Players[0] != NULL)
154 {
155 camera->HUD.DrawData.NumScreens = 1;
156 camera->lastPosition = GetFollowPlayerPos(
157 camera->lastPosition, camera->HUD.DrawData.Players[0]);
158 }
159 }
160 }
161
162 HUDUpdate(&camera->HUD, ms);
163 camera->shake = ScreenShakeUpdate(camera->shake, ticks);
164
165 // Determine how many camera views to use, and set camera position
166 if (camera->HUD.DrawData.NumScreens == 0)
167 {
168 camera->NumViews = 1;
169 SoundSetEars(camera->lastPosition);
170 }
171 else
172 {
173 const bool onePlayer = camera->HUD.DrawData.NumScreens == 1;
174 const bool singleScreen = CameraIsSingleScreen();
175 if (onePlayer || singleScreen)
176 {
177 // Single camera screen
178 if (onePlayer)
179 {
180 const PlayerData *firstPlayer =
181 camera->HUD.DrawData.Players[0];
182 const TActor *p = ActorGetByUID(firstPlayer->ActorUID);
183 camera->lastPosition = p->thing.Pos;
184 }
185 else if (singleScreen)
186 {
187 // One screen
188 camera->lastPosition = PlayersGetMidpoint();
189 }
190
191 // Special case: if map is smaller than screen, center the camera
192 // However, it is important to keep the ear positions unmodified
193 // so that sounds don't get muffled just because there's a wall
194 // between player and camera center
195 const struct vec2 earPos = camera->lastPosition;
196 if (gMap.Size.x * TILE_WIDTH < gGraphicsDevice.cachedConfig.Res.x)
197 {
198 camera->lastPosition.x = (float)gMap.Size.x * TILE_WIDTH / 2;
199 }
200 if (gMap.Size.y * TILE_HEIGHT < gGraphicsDevice.cachedConfig.Res.y)
201 {
202 camera->lastPosition.y = (float)gMap.Size.y * TILE_HEIGHT / 2;
203 }
204
205 SoundSetEars(earPos);
206
207 camera->NumViews = 1;
208 }
209 else if (camera->HUD.DrawData.NumScreens == 2)
210 {
211 // side-by-side split
212 for (int i = 0; i < camera->HUD.DrawData.NumScreens; i++)
213 {
214 const PlayerData *p = camera->HUD.DrawData.Players[i];
215 const TActor *a = ActorGetByUID(p->ActorUID);
216 camera->lastPosition = a->thing.Pos;
217 SoundSetEarsSide(i == 0, camera->lastPosition);
218 }
219
220 camera->NumViews = 2;
221 }
222 else if (camera->HUD.DrawData.NumScreens >= 3 &&
223 camera->HUD.DrawData.NumScreens <= 4)
224 {
225 // 4 player split screen
226 bool isLocalPlayerAlive[MAX_LOCAL_PLAYERS];
227 memset(isLocalPlayerAlive, 0, sizeof isLocalPlayerAlive);
228 for (int i = 0; i < camera->HUD.DrawData.NumScreens; i++)
229 {
230 const PlayerData *p = camera->HUD.DrawData.Players[i];
231 isLocalPlayerAlive[i] = IsPlayerAliveOrDying(p);
232 if (!isLocalPlayerAlive[i])
233 {
234 continue;
235 }
236 const TActor *a = ActorGetByUID(p->ActorUID);
237 camera->lastPosition = a->thing.Pos;
238
239 // Set the sound "ears"
240 const bool isLeft = i == 0 || i == 2;
241 const bool isUpper = i <= 2;
242 SoundSetEar(isLeft, isUpper ? 0 : 1, camera->lastPosition);
243 // If any player is dead, that ear reverts to the other ear
244 // of the same side of the remaining player
245 const int otherIdxOnSameSide = i ^ 2;
246 if (!isLocalPlayerAlive[otherIdxOnSameSide])
247 {
248 SoundSetEar(
249 isLeft, !isUpper ? 0 : 1, camera->lastPosition);
250 }
251 else if (!isLocalPlayerAlive[3 - i] &&
252 !isLocalPlayerAlive[3 - otherIdxOnSameSide])
253 {
254 // If both players of one side are dead,
255 // those ears revert to any of the other remaining
256 // players
257 SoundSetEarsSide(!isLeft, camera->lastPosition);
258 }
259 }
260
261 camera->NumViews = 4;
262 }
263 else
264 {
265 CASSERT(false, "not implemented yet");
266 }
267 }
268 }
269 // Try to follow a player
GetFollowPlayerPos(const struct vec2 lastPos,const PlayerData * p)270 static struct vec2 GetFollowPlayerPos(
271 const struct vec2 lastPos, const PlayerData *p)
272 {
273 const TActor *a = ActorGetByUID(p->ActorUID);
274 if (a == NULL) return lastPos;
275 return a->Pos;
276 }
277
278 static void DoBuffer(
279 DrawBuffer *b, const struct vec2 center, const int w, const struct vec2 noise,
280 const struct vec2i offset);
CameraDraw(Camera * camera,const HUDDrawData drawData)281 void CameraDraw(Camera *camera, const HUDDrawData drawData)
282 {
283 const struct vec2i centerOffset = svec2i(-4, -8);
284 const int w = gGraphicsDevice.cachedConfig.Res.x;
285 const int h = gGraphicsDevice.cachedConfig.Res.y;
286
287 const struct vec2 noise = camera->shake.Delta;
288
289 GraphicsResetClip(gGraphicsDevice.gameWindow.renderer);
290 if (drawData.NumScreens == 0)
291 {
292 DoBuffer(
293 &camera->Buffer,
294 camera->lastPosition,
295 X_TILES, noise, centerOffset);
296 }
297 else
298 {
299 // Redo LOS if PVP, so that each split screen has its own LOS
300 if (IsPVP(gCampaign.Entry.Mode) && drawData.NumScreens > 0)
301 {
302 LOSReset(&gMap.LOS);
303 }
304 if (camera->NumViews == 1)
305 {
306 // Single camera screen
307
308 // Redo LOS for every local human player
309 if (IsPVP(gCampaign.Entry.Mode))
310 {
311 CA_FOREACH(const PlayerData, p, gPlayerDatas)
312 if (!p->IsLocal || !IsPlayerAliveOrDying(p) ||
313 !IsPlayerHuman(p))
314 {
315 continue;
316 }
317 const TActor *a = ActorGetByUID(p->ActorUID);
318 LOSCalcFrom(&gMap, Vec2ToTile(a->thing.Pos), false);
319 CA_FOREACH_END()
320 }
321
322 DoBuffer(
323 &camera->Buffer,
324 camera->lastPosition,
325 X_TILES, noise, centerOffset);
326 }
327 else if (drawData.NumScreens == 2)
328 {
329 // side-by-side split
330 for (int i = 0; i < drawData.NumScreens; i++)
331 {
332 const PlayerData *p = drawData.Players[i];
333 if (!IsPlayerAliveOrDying(p))
334 {
335 continue;
336 }
337 const TActor *a = ActorGetByUID(p->ActorUID);
338 if (a == NULL)
339 {
340 continue;
341 }
342 camera->lastPosition = a->thing.Pos;
343 struct vec2i centerOffsetPlayer = centerOffset;
344 const Rect2i clip = Rect2iNew(
345 svec2i((i & 1) ? w / 2 : 0, 0), svec2i(w / 2, h));
346 GraphicsSetClip(gGraphicsDevice.gameWindow.renderer, clip);
347 if (i == 1)
348 {
349 centerOffsetPlayer.x += w / 2 - centerOffset.x;
350 }
351
352 LOSCalcFrom(&gMap, Vec2ToTile(camera->lastPosition), false);
353 DoBuffer(
354 &camera->Buffer,
355 camera->lastPosition,
356 X_TILES_HALF, noise, centerOffsetPlayer);
357 }
358 Draw_Line(w / 2 - 1, 0, w / 2 - 1, h - 1, colorBlack);
359 Draw_Line(w / 2, 0, w / 2, h - 1, colorBlack);
360 }
361 else if (drawData.NumScreens >= 3 && drawData.NumScreens <= 4)
362 {
363 // 4 player split screen
364 for (int i = 0; i < drawData.NumScreens; i++)
365 {
366 const PlayerData *p = drawData.Players[i];
367 if (!IsPlayerAliveOrDying(p))
368 {
369 continue;
370 }
371 const TActor *a = ActorGetByUID(p->ActorUID);
372 if (a == NULL)
373 {
374 continue;
375 }
376 camera->lastPosition = a->thing.Pos;
377 struct vec2i centerOffsetPlayer = centerOffset;
378 const Rect2i clip = Rect2iNew(
379 svec2i((i & 1) ? w / 2 : 0, (i < 2) ? 0 : h / 2 - 1),
380 svec2i(w / 2, h / 2));
381 GraphicsSetClip(gGraphicsDevice.gameWindow.renderer, clip);
382 if (i & 1)
383 {
384 centerOffsetPlayer.x += w / 2 - centerOffset.x;
385 }
386 if (i < 2)
387 {
388 centerOffsetPlayer.y -= h / 4 + centerOffset.y;
389 }
390 else
391 {
392 centerOffsetPlayer.y += h / 4 - centerOffset.y;
393 }
394 LOSCalcFrom(&gMap, Vec2ToTile(camera->lastPosition), false);
395 DoBuffer(
396 &camera->Buffer,
397 camera->lastPosition,
398 X_TILES_HALF, noise, centerOffsetPlayer);
399 }
400 Draw_Line(w / 2 - 1, 0, w / 2 - 1, h - 1, colorBlack);
401 Draw_Line(w / 2, 0, w / 2, h - 1, colorBlack);
402 Draw_Line(0, h / 2 - 1, w - 1, h / 2 - 1, colorBlack);
403 Draw_Line(0, h / 2, w - 1, h / 2, colorBlack);
404 }
405 else
406 {
407 assert(0 && "not implemented yet");
408 }
409 }
410 GraphicsResetClip(gGraphicsDevice.gameWindow.renderer);
411 }
DoBuffer(DrawBuffer * b,const struct vec2 center,const int w,const struct vec2 noise,const struct vec2i offset)412 static void DoBuffer(
413 DrawBuffer *b, const struct vec2 center, const int w, const struct vec2 noise,
414 const struct vec2i offset)
415 {
416 DrawBufferSetFromMap(b, &gMap, svec2_add(center, noise), w);
417 if (gPlayerDatas.size > 0)
418 {
419 DrawBufferFix(b);
420 }
421 DrawBufferDraw(b, offset, NULL);
422 }
423
CameraDrawMode(const Camera * camera)424 void CameraDrawMode(const Camera *camera)
425 {
426 // Draw camera mode
427 char cameraNameBuf[256];
428 bool drawCameraMode = false;
429 switch (camera->spectateMode)
430 {
431 case SPECTATE_NONE:
432 // do nothing
433 break;
434 case SPECTATE_FOLLOW:
435 {
436 const TActor *a = ActorGetByUID(camera->FollowActorUID);
437 if (a == NULL || !a->isInUse) break;
438 const PlayerData *p = PlayerDataGetByUID(a->PlayerUID);
439 if (p == NULL) break;
440 sprintf(cameraNameBuf, "Following %s", p->name);
441 drawCameraMode = true;
442 }
443 break;
444 case SPECTATE_FREE:
445 strcpy(cameraNameBuf, "Free-look Mode");
446 drawCameraMode = true;
447 break;
448 default:
449 CASSERT(false, "Unknown spectate mode");
450 break;
451 }
452 if (!drawCameraMode)
453 {
454 return;
455 }
456
457 const int w = gGraphicsDevice.cachedConfig.Res.x;
458 const int h = gGraphicsDevice.cachedConfig.Res.y;
459
460 // Draw the message centered at the bottom
461 FontStrMask(
462 cameraNameBuf,
463 svec2i((w - FontStrW(cameraNameBuf)) / 2, h - FontH() * 2),
464 colorYellow);
465
466 // Show camera controls
467 const PlayerData *p = GetFirstPlayer(false, true, true);
468 // Use default keyboard controls
469 input_device_e inputDevice = INPUT_DEVICE_KEYBOARD;
470 int deviceIndex = 0;
471 if (p != NULL)
472 {
473 inputDevice = p->inputDevice;
474 deviceIndex = p->deviceIndex;
475 }
476 struct vec2i pos = svec2i(
477 (w - FontStrW("foo/bar to follow player, baz to free-look")) / 2,
478 h - FontH());
479 char buf[256];
480 color_t c = colorYellow;
481 InputGetButtonNameColor(
482 inputDevice, deviceIndex, CMD_BUTTON1, buf, &c);
483 pos = FontStrMask(buf, pos, c);
484 pos = FontStrMask("/", pos, colorYellow);
485 c = colorYellow;
486 InputGetButtonNameColor(
487 inputDevice, deviceIndex, CMD_BUTTON2, buf, &c);
488 pos = FontStrMask(buf, pos, c);
489 pos = FontStrMask(" to follow player, ", pos, colorYellow);
490 InputGetDirectionNames(buf, inputDevice, deviceIndex);
491 pos = FontStrMask(buf, pos, colorYellow);
492 FontStrMask(" to free-look", pos, colorYellow);
493 }
494
CameraIsSingleScreen(void)495 bool CameraIsSingleScreen(void)
496 {
497 if (ConfigGetEnum(&gConfig, "Interface.Splitscreen") == SPLITSCREEN_ALWAYS)
498 {
499 return false;
500 }
501 // Do split screen for PVP, unless whole map fits on camera, or there
502 // are no human players alive or dying, then just do single screen
503 const bool mapFitsInScreen =
504 gMap.Size.x * TILE_WIDTH < gGraphicsDevice.cachedConfig.Res.x &&
505 gMap.Size.y * TILE_HEIGHT < gGraphicsDevice.cachedConfig.Res.y;
506 if (IsPVP(gCampaign.Entry.Mode) &&
507 !mapFitsInScreen &&
508 GetNumPlayers(PLAYER_ALIVE_OR_DYING, true, true) > 0)
509 {
510 return false;
511 }
512 // Otherwise, if we are forcing never splitscreen, use single screen
513 // regardless of whether the players are within camera range
514 if (ConfigGetEnum(&gConfig, "Interface.Splitscreen") == SPLITSCREEN_NEVER)
515 {
516 return true;
517 }
518 // Finally, use split screen if players don't fit on camera
519 struct vec2 min, max;
520 PlayersGetBoundingRectangle(&min, &max);
521 return
522 max.x - min.x < gGraphicsDevice.cachedConfig.Res.x - CAMERA_SPLIT_PADDING &&
523 max.y - min.y < gGraphicsDevice.cachedConfig.Res.y - CAMERA_SPLIT_PADDING;
524 }
525