aboutsummaryrefslogtreecommitdiffstats
path: root/examples/bullet_engine/bullet_engine.erl
diff options
context:
space:
mode:
Diffstat (limited to 'examples/bullet_engine/bullet_engine.erl')
-rw-r--r--examples/bullet_engine/bullet_engine.erl251
1 files changed, 251 insertions, 0 deletions
diff --git a/examples/bullet_engine/bullet_engine.erl b/examples/bullet_engine/bullet_engine.erl
new file mode 100644
index 0000000..0006bcd
--- /dev/null
+++ b/examples/bullet_engine/bullet_engine.erl
@@ -0,0 +1,251 @@
+%% This is an example. Feel free to copy and reuse as you wish.
+
+-module(bullet_engine).
+-export([run/0]).
+
+run() ->
+ spawn(fun init/0).
+
+init() ->
+ ok = sdl:start([video]),
+ ok = sdl:stop_on_exit(),
+ {ok, Window} = sdl_window:create("Hello SDL", 10, 10, 500, 600, []),
+ {ok, Renderer} = sdl_renderer:create(Window, -1, [accelerated, present_vsync]),
+ ok = sdl_renderer:set_logical_size(Renderer, 500 bsl 16, 600 bsl 16),
+ {ok, Texture} = sdl_texture:create_from_file(Renderer, "bullet.png"),
+ loop(#{window=>Window, renderer=>Renderer, texture=>Texture,
+ scene=>init_scene()}).
+
+loop(State=#{scene:=Scene}) ->
+ events_loop(),
+ Scene2 = update_scene(Scene, []),
+ State2 = State#{scene:=Scene2},
+ render(State2),
+ loop(State2).
+
+events_loop() ->
+ case sdl_events:poll() of
+ false -> ok;
+ #{type:=quit} -> terminate();
+ _ -> events_loop()
+ end.
+
+render(#{renderer:=Renderer, texture:=Texture, scene:=Scene}) ->
+ ok = sdl_renderer:clear(Renderer),
+ _ = [sdl_renderer:copy(Renderer, Texture, undefined, Bullet)
+ || Bullet = #{t:=Type} <- Scene, Type =/= invisible],
+ ok = sdl_renderer:present(Renderer).
+
+terminate() ->
+ init:stop(),
+ exit(normal).
+
+%% Demo scene.
+
+init_scene() ->
+ [new_invisible(#{x=>250 bsl 16, y=>300 bsl 16, w=>0, h=>0, actions=>[
+ %% Part 1.
+ {var, w, 31},
+ {var, x, 0},
+ {loop, 30, [
+ {loop, 18, [
+ {fire, [
+ {set, speed, 2 bsl 16},
+ {set, dir, x}
+ ]},
+ {var, x, '+=', 30}
+ ]},
+ {wait, w},
+ {var, w, '+=', -1}
+ ]},
+
+ %% Part 2.
+ {var, y, 31},
+ {var, z, -3},
+ {loop, 2, [
+ {loop, 21, [
+ {loop, 18, [
+ {fire, [
+ {set, speed, 2 bsl 16},
+ {set, dir, x}
+ ]},
+ {var, x, '+=', y},
+ {wait, 1}
+ ]},
+ {var, y, '+=', z}
+ ]},
+ {var, z, '*=', -1}
+ ]},
+
+ %% Part 3.
+ {var, i, 45},
+ {var, n, 4},
+ {loop, 4, [
+ {wait, 60},
+ {fire, [
+ {set, w, 64 bsl 16},
+ {set, h, 64 bsl 16},
+ {set, dir, i},
+ {set, speed, 2 bsl 16},
+ {loop, 10, [
+ {fire, [
+ {var, j, 180},
+ {var, j, '+=', i},
+ {set, dir, j},
+ {set, speed, 10 bsl 16}
+ ]},
+ {wait, 6}
+ ]},
+ {set, speed, 0},
+ {var, w, 60},
+ {var, w, '*=', n},
+ {wait, w},
+ {var, d, 90},
+ {var, n, '+=', -4},
+ {var, n, '*=', -1},
+ {var, d, '*=', n},
+ {var, d, '+=', -225},
+ {set, dir, d},
+ {set, speed, 3 bsl 16},
+ {loop, 120, [
+ {var, d, '+=', 3},
+ {set, dir, d},
+ {wait, 1}
+ ]},
+ {loop, 120, [
+ {fire, []},
+ {loop, 3, [
+ {var, d, '+=', 3},
+ {set, dir, d},
+ {wait, 1}
+ ]}
+ ]},
+ {loop, 120, [
+ {fire, []},
+ {var, d2, 180},
+ {var, d2, '+=', d},
+ {fire, [
+ {set, dir, d2}
+ ]},
+ {loop, 3, [
+ {var, d, '+=', 3},
+ {set, dir, d},
+ {wait, 1}
+ ]}
+ ]},
+ {loop, 120, [
+ {fire, []},
+ {var, d2, 180},
+ {var, d2, '+=', d},
+ {fire, [
+ {set, dir, d2}
+ ]},
+ {loop, 2, [
+ {var, d, '+=', 3},
+ {set, dir, d},
+ {wait, 1}
+ ]}
+ ]},
+ {loop, 120, [
+ {fire, []},
+ {var, d2, 180},
+ {var, d2, '+=', d},
+ {fire, [
+ {set, dir, d2}
+ ]},
+ {var, d, '+=', 3},
+ {set, dir, d},
+ {wait, 1}
+ ]},
+ {wait, 240}
+ ]},
+ {var, i, '+=', 90},
+ {var, n, '+=', -1}
+ ]},
+
+ %% Wait for the scene to finish, then stop the VM.
+ {wait, 2640},
+ init_stop
+ ]})].
+
+update_scene([], Acc) ->
+ lists:flatten(Acc);
+%% We avoid float arithmetic where possible.
+%% For Pi we use the approximate fraction 103993/33102.
+%% We may also want to do an integer cosine and sine.
+update_scene([Bullet = #{x:=X, y:=Y, w:=W, h:=H, dir:=Dir, speed:=Speed,
+ wait:=Wait, actions:=Actions}|Tail], Acc) ->
+ A = (103993 * (Dir - 90)) / (33102 * 180),
+ X2 = X + round(Speed * math:cos(A)),
+ Y2 = Y + round(Speed * math:sin(A)),
+ if
+ Wait > 0 ->
+ update_scene(Tail, [Bullet#{x:=X2, y:=Y2, wait:=Wait - 1}|Acc]);
+ X2 > 500 bsl 16; X2 < -W; Y2 > 600 bsl 16; Y2 < -H ->
+ update_scene(Tail, Acc);
+ true ->
+ New = update_bullet(Bullet#{x:=X2, y:=Y2}, Actions, []),
+ update_scene(Tail, [New|Acc])
+ end.
+
+%% Bullet engine.
+%%
+%% The scene is (500 bsl 16)x(600 bsl 16) rendered as 500x600. We avoid floats for
+%% performance reasons so everything only goes up to 3 decimals, which is perfectly
+%% fine anyway.
+%%
+%% Execution is done frame by frame for simplicity, relying on vsync. Another
+%% advantage of doing this is that we can very easy record a replay based on user
+%% input, although we don't have any in this small demo.
+
+new_invisible(Parent) ->
+ Parent#{t=>invisible, w=>0, h=>0, dir=>0, speed=>0,
+ wait=>0, vars=>#{}}.
+
+new_bullet(Parent=#{x:=X, y:=Y, w:=W, h:=H}, Actions) ->
+ Parent#{t=>bullet, x=>X + (W div 2) - (8 bsl 16), y=>Y + (H div 2) - (8 bsl 16),
+ w=>16 bsl 16, h=>16 bsl 16, wait=>0, actions=>Actions}.
+
+update_bullet(Bullet, [], Acc) ->
+ [Bullet#{actions:=[]}|Acc];
+%% Stop the VM.
+update_bullet(Bullet, [init_stop|_], Acc) ->
+ init:stop(),
+ update_bullet(Bullet, [], Acc);
+%% Manipulate variables.
+update_bullet(Bullet=#{vars:=Vars}, [{var, V, Value}|Tail], Acc) ->
+ update_bullet(Bullet#{vars:=maps:put(V, Value, Vars)}, Tail, Acc);
+update_bullet(Bullet=#{vars:=Vars}, [{var, V, '+=', W}|Tail], Acc) when is_atom(W) ->
+ update_bullet(Bullet#{vars:=maps:put(V, maps:get(V, Vars) + maps:get(W, Vars), Vars)}, Tail, Acc);
+update_bullet(Bullet=#{vars:=Vars}, [{var, V, '+=', W}|Tail], Acc) ->
+ update_bullet(Bullet#{vars:=maps:put(V, maps:get(V, Vars) + W, Vars)}, Tail, Acc);
+update_bullet(Bullet=#{vars:=Vars}, [{var, V, '*=', W}|Tail], Acc) when is_atom(W) ->
+ update_bullet(Bullet#{vars:=maps:put(V, maps:get(V, Vars) * maps:get(W, Vars), Vars)}, Tail, Acc);
+update_bullet(Bullet=#{vars:=Vars}, [{var, V, '*=', W}|Tail], Acc) ->
+ update_bullet(Bullet#{vars:=maps:put(V, maps:get(V, Vars) * W, Vars)}, Tail, Acc);
+%% Loop actions.
+%%
+%% We only unroll one iteration at a time to avoid wasting resources.
+update_bullet(Bullet, [{loop, 1, Actions}|Tail], Acc) ->
+ update_bullet(Bullet, Actions ++ Tail, Acc);
+update_bullet(Bullet, [{loop, N, Actions}|Tail], Acc) ->
+ update_bullet(Bullet, Actions ++ [{loop, N - 1, Actions}|Tail], Acc);
+%% Wait a few frames.
+update_bullet(Bullet=#{vars:=Vars}, [{wait, V}|Tail], Acc) when is_atom(V) ->
+ [Bullet#{wait:=maps:get(V, Vars), actions:=Tail}|Acc];
+update_bullet(Bullet, [{wait, N}|Tail], Acc) ->
+ [Bullet#{wait:=N, actions:=Tail}|Acc];
+%% Fire a new bullet.
+update_bullet(Bullet, [{fire, Actions}|Tail], Acc) ->
+ update_bullet(Bullet, Tail, [new_bullet(Bullet, Actions)|Acc]);
+%% Set bullet values directly.
+update_bullet(Bullet=#{vars:=Vars}, [{set, Key, V}|Tail], Acc) when is_atom(V) ->
+ update_bullet(maps:put(Key, maps:get(V, Vars), Bullet), Tail, Acc);
+update_bullet(Bullet=#{x:=X, y:=Y, w:=W, h:=H}, [{set, Key, Value}|Tail], Acc) ->
+ %% We need to reposition the bullet if the size changes,
+ %% are the bullet position is its top left corner.
+ case Key of
+ w -> update_bullet(maps:put(Key, Value, Bullet#{x:=X + ((W - Value) div 2)}), Tail, Acc);
+ h -> update_bullet(maps:put(Key, Value, Bullet#{y:=Y + ((H - Value) div 2)}), Tail, Acc);
+ _ -> update_bullet(maps:put(Key, Value, Bullet), Tail, Acc)
+ end.