%% 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, 12, [
{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.