File: game.pas

package info (click to toggle)
castle-game-engine 6.4%2Bdfsg1-2
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 194,520 kB
  • sloc: pascal: 364,585; ansic: 8,606; java: 2,851; objc: 2,601; cpp: 1,412; xml: 851; makefile: 725; sh: 563; php: 26
file content (327 lines) | stat: -rw-r--r-- 10,421 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
{
  Copyright 2011-2017 Michalis Kamburelis.

  This file is part of "Castle Game Engine".

  "Castle Game Engine" is free software; see the file COPYING.txt,
  included in this distribution, for details about the copyright.

  "Castle Game Engine" is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

  ----------------------------------------------------------------------------
}

{ Game initialization and logic. }
unit Game;

interface

implementation

uses
  { standard units }
  SysUtils, Math,
  { Castle Game Engine units }
  CastleWindow, CastleFilesUtils, CastleWindowModes, CastleStringUtils,
  CastleUtils, CastleGLUtils, CastleKeysMouse, CastleMessages, CastleGLImages,
  CastleImages, CastleColors, CastleLog, CastleApplicationProperties,
  { game units }
  GameMap, GamePlayer, GameWindow;

var
  Player: TPlayer;
  ViewMoveX, ViewMoveY: Single;
  ViewFollowsPlayer: boolean = true;

procedure WindowRender(Container: TUIContainer);
var
  RealViewMoveX, RealViewMoveY: Integer;

  procedure DrawImageOnTile(X, Y: Cardinal; GLImage: TGLImage;
    const SpecialMoveX: Integer = 0;
    const SpecialMoveY: Integer = 0);
  var
    PosX, PosY: Integer;
  begin
    PosX := X * BaseWidth;
    if Odd(Y) then
      PosX += BaseWidth div 2;
    PosX += RealViewMoveX + SpecialMoveX;
    PosY := Y * (BaseHeight div 2);
    PosY += RealViewMoveY + SpecialMoveY;
    GLImage.Alpha := acTest;
    GLImage.Draw(PosX, PosY);
  end;

var
  X, Y: Cardinal;
  MapTile: TMapTile;
  BaseFitX, BaseFitY: Cardinal;
  X1, X2, Y1, Y2: Integer;
begin
  RenderContext.Clear([cbColor], Black);

  BaseFitX := Ceil(Window.Width / BaseWidth) + 1;
  BaseFitY := Ceil(2 * Window.Height / BaseHeight) + 1;

  if ViewFollowsPlayer then
  begin
    { Ignore ViewMoveX/Y, calculate RealView such that the player
      is in the middle. }
    RealViewMoveX := Player.XPixel;
    RealViewMoveY := Player.YPixel;
    if Player.Moving then
    begin
      RealViewMoveX -= Round(Player.MovingSmallMoveX);
      RealViewMoveY -= Round(Player.MovingSmallMoveY);
    end;
  end else
  begin
    RealViewMoveX := Round(ViewMoveX);
    RealViewMoveY := Round(ViewMoveY);
  end;

  { First: this is what would be seen if RealViewMoveX/Y is zero. }
  X1 := -1;
  X2 := Integer(BaseFitX) - 2;
  Y1 := -1;
  Y2 := Integer(BaseFitY) - 2;
  { Now translate taking RealViewMoveX/Y into account. }
  X1 -= Ceil(RealViewMoveX / BaseWidth);
  X2 -= Floor(RealViewMoveX / BaseWidth);
  Y1 -= Ceil(2 * RealViewMoveY / BaseHeight);
  Y2 -= Floor(2 * RealViewMoveY / BaseHeight);
  { Eventually correct to be inside 0..Map.Width/Height - 1 range }
  ClampVar(X1, 0, Map.Width - 1);
  ClampVar(X2, 0, Map.Width - 1);
  ClampVar(Y1, 0, Map.Height - 1);
  ClampVar(Y2, 0, Map.Height - 1);

  for X := X1 to X2 do
    for Y := Y1 to Y2 do
    begin
      MapTile := Map.Items[X, Y];
      DrawImageOnTile(X, Y, MapTile.BaseTile.GLImage);
    end;

  { TODO: unoptimal code, should draw only the part that fits within the window.
    We should auto-check width/height of bonus tile, to know when to draw it.
    Even better, we should record this on the map --- which tile is visible
    where. }
  for Y := Map.Height - 1 downto 0 do
  begin
    { The order of drawing is important. Player must be drawn
      on top of some objects and below some others. }
    if Y = Player.Y then
    begin
      if Player.Moving then
        DrawImageOnTile(Player.X, Player.Y, Player.GLImage[Player.Direction],
          Round(Player.MovingSmallMoveX),
          Round(Player.MovingSmallMoveY)) else
        DrawImageOnTile(Player.X, Player.Y, Player.GLImage[Player.Direction]);
    end;

    for X := 0 to Map.Width - 1 do
    begin
      MapTile := Map.Items[X, Y];
      if MapTile.BonusTile <> nil then
        DrawImageOnTile(X, Y, MapTile.BonusTile.GLImage);
    end;
  end;
end;

procedure WindowPress(Container: TUIContainer; const Event: TInputPressRelease);
var
  NewViewMoveX, NewViewMoveY: Integer;

  { Get character from user. Returns #0 if cancelled. }
  function MessageChar(const S: string): char;
  var
    Event: TInputPressRelease;
  begin
    Event := MessageKeyMouse(Window,
      'Enter the character code of new base tile, or Escape to cancel');
    if (Event.EventType = itKey) and
       (Event.KeyCharacter <> CharEscape) and
       (Event.KeyCharacter <> #0)  then
      Result := Event.KeyCharacter else
      Result := #0;
  end;

  procedure EditBaseTile;
  var
    BaseTile: TBaseTile;
    C: Char;
  begin
    C := MessageChar('Enter the character code of new base tile, or Escape to cancel');
    if C <> #0 then
    begin
      BaseTile := Map.BaseTiles[C];
      if BaseTile = nil then
        MessageOK(Window, Format('The character "%s" is not a code ' +
          'for any base tile', [C])) else
      Map.Items[Player.X, Player.Y].BaseTile := BaseTile;
    end;
  end;

  procedure EditBonusTile;
  var
    BonusTile: TBonusTile;
    C: Char;
  begin
    C := MessageChar('Enter the character code of new bonus tile, or "_" to clear or Escape to cancel');
    if C <> #0 then
    begin
      if C = '_' then
        Map.Items[Player.X, Player.Y].BonusTile := nil else
      begin
        BonusTile := Map.BonusTiles[C];
        if BonusTile = nil then
          MessageOK(Window, Format('The character "%s" is not a code ' +
            'for any bonus tile', [C])) else
        Map.Items[Player.X, Player.Y].BonusTile := BonusTile;
      end;
    end;
  end;

  procedure ShowFieldInfo;

    function TileDescr(Tile: TTile): string;
    begin
      if Tile = nil then
        Result := '<none>' else
        Result := Format('"%s" (URL "%s")',
          [Tile.CharCode, Tile.RelativeURL]);
    end;

  begin
    MessageOK(Window, Format(
      'Position: %d, %d' +nl+
      'Base tile: %s' +nl+
      'Bonus tile: %s',
      [ Player.X, Player.Y,
        TileDescr(Map.Items[Player.X, Player.Y].BaseTile),
        TileDescr(Map.Items[Player.X, Player.Y].BonusTile) ]));
  end;

var
  URL: string;
begin
  if Event.EventType = itKey then
  begin
    case Event.KeyCharacter of
      'f': begin
             ViewFollowsPlayer := not ViewFollowsPlayer;
             if not ViewFollowsPlayer then
             begin
               { Set ViewMoveX/Y initial values such that the player is still
                 in the middle. This is less confusing for user. }
               ViewMoveToCenterPosition(Player.X, Player.Y,
                 NewViewMoveX, NewViewMoveY);
               ViewMoveX := NewViewMoveX;
               ViewMoveY := NewViewMoveY;
             end;
           end;
      'e': EditBaseTile;
      'E': EditBonusTile;
      's': begin
             URL := 'new';
             if MessageInputQuery(Window, 'Save map as name' +
               ' (don''t specify here initial path and .map extension)', URL) then
               Map.SaveToFile(ApplicationData('maps/' + URL + '.map'));
           end;
      'i': ShowFieldInfo;
      CharEscape: Window.Close;
    end;
  end;
end;

procedure WindowUpdate(Container: TUIContainer);
const
  ViewMoveChangeSpeed = 10.0 * 50.0;
begin
  if not ViewFollowsPlayer then
  begin
    if Window.Pressed[K_Up]    then ViewMoveY -= ViewMoveChangeSpeed * Window.Fps.SecondsPassed;
    if Window.Pressed[K_Down]  then ViewMoveY += ViewMoveChangeSpeed * Window.Fps.SecondsPassed;
    if Window.Pressed[K_Right] then ViewMoveX -= ViewMoveChangeSpeed * Window.Fps.SecondsPassed;
    if Window.Pressed[K_Left]  then ViewMoveX += ViewMoveChangeSpeed * Window.Fps.SecondsPassed;
  end else
  begin
    { At first I placed the commands below in KeyDown, as they work
      like KeyDown: non-continuously. However, thanks to smooth scrolling
      of the screen, user is easily fooled and thinks that they work
      continuously. So he keeps pressing them. So we should check them
      here. }
    if Window.Pressed[K_Up]    then Player.Move(dirNorth);
    if Window.Pressed[K_Down]  then Player.Move(dirSouth);
    if Window.Pressed[K_Left]  then Player.Move(dirWest);
    if Window.Pressed[K_Right] then Player.Move(dirEast);

    if Window.Pressed[K_Numpad_7] then Player.Move(dirNorthWest);
    if Window.Pressed[K_Numpad_9] then Player.Move(dirNorthEast);
    if Window.Pressed[K_Numpad_1] then Player.Move(dirSouthWest);
    if Window.Pressed[K_Numpad_3] then Player.Move(dirSouthEast);
    if Window.Pressed[K_Numpad_4] then Player.Move(dirWest);
    if Window.Pressed[K_Numpad_6] then Player.Move(dirEast);
    if Window.Pressed[K_Numpad_2] then Player.Move(dirSouth);
    if Window.Pressed[K_Numpad_8] then Player.Move(dirNorth);

    if Window.Pressed[K_F10] then
    begin
      { simulate OpenGL context close + open, this may happen at any time on Android/iOS }
      Window.Close(false);
      Window.Open;
    end;
  end;

  GameTime += Window.Fps.SecondsPassed;

  Player.Update;
end;

{ One-time initialization of resources. }
procedure ApplicationInitialize;
begin
  { Assign Window callbacks }
  Window.OnUpdate := @WindowUpdate;
  Window.OnPress := @WindowPress;
  Window.OnRender := @WindowRender;

  { For a scalable UI (adjusts to any window size in a smart way), use UIScaling }
  // Window.Container.UIReferenceWidth := 1024;
  // Window.Container.UIReferenceHeight := 768;
  // Window.Container.UIScaling := usEncloseReferenceSize;

  Map := TMap.CreateFromFile(ApplicationData('maps/1.map'));
  Player := TPlayer.Create;
  Player.Teleport(Map.PlayerStartX, Map.PlayerStartY, dirSouth);
  Player.CalculatePixelPosition;
end;

initialization
  { Set ApplicationName early, as our log uses it.
    Optionally you could also set ApplicationProperties.Version here. }
  ApplicationProperties.ApplicationName := 'isometric_game';

  { Start logging. Do this as early as possible,
    to log information and eventual warnings during initialization. }
  InitializeLog;

  { Initialize Application.OnInitialize. }
  Application.OnInitialize := @ApplicationInitialize;

  { Create and assign Application.MainWindow. }
  Window := TCastleWindowCustom.Create(Application);
  Application.MainWindow := Window;

  { Our drawing routine is not prepared to react perfectly to window size change
    at runtime. So disable it for now. }
  Window.ResizeAllowed := raOnlyAtOpen;
  Window.FpsShowOnCaption := true;
finalization
  FreeAndNil(Player);
  FreeAndNil(Map);
end.