├── .formatter.exs
├── .gitignore
├── LICENSE
├── README.md
├── config
└── config.exs
├── data.xml
├── lib
├── breakout.ex
└── breakout
│ ├── application.ex
│ ├── audio.ex
│ ├── ball_object.ex
│ ├── game.ex
│ ├── game_level.ex
│ ├── game_object.ex
│ ├── image_parser.ex
│ ├── input.ex
│ ├── logger.ex
│ ├── math
│ ├── mat2.ex
│ ├── mat3.ex
│ ├── mat4.ex
│ ├── quat.ex
│ ├── vec2.ex
│ ├── vec3.ex
│ └── vec4.ex
│ ├── particle.ex
│ ├── particle_generator.ex
│ ├── player.ex
│ ├── power_up.ex
│ ├── renderer
│ ├── open_gl.ex
│ ├── post_processor.ex
│ ├── renderer.ex
│ ├── shader.ex
│ ├── sprite.ex
│ ├── text.ex
│ ├── texture2d.ex
│ └── window.ex
│ ├── resource_manager.ex
│ ├── state.ex
│ ├── util.ex
│ └── wx_records.ex
├── mix.exs
├── mix.lock
├── priv
├── audio
│ ├── block.mp3
│ ├── breakout.mp3
│ ├── paddle.mp3
│ ├── powerup.mp3
│ └── solid.mp3
├── levels
│ ├── four.lvl
│ ├── one.lvl
│ ├── three.lvl
│ └── two.lvl
├── shaders
│ ├── particle
│ │ ├── fragment.fs
│ │ └── vertex.vs
│ ├── post_processor
│ │ ├── fragment.fs
│ │ └── vertex.vs
│ └── sprite
│ │ ├── fragment.fs
│ │ └── vertex.vs
├── sprites
│ └── test.png
└── textures
│ ├── ascii.png
│ ├── ascii_rgb.png
│ ├── awesomeface.png
│ ├── background.jpg
│ ├── background.png
│ ├── block.png
│ ├── block_solid.png
│ ├── paddle.png
│ ├── particle.png
│ ├── powerup_chaos.png
│ ├── powerup_confuse.png
│ ├── powerup_increase.png
│ ├── powerup_passthrough.png
│ ├── powerup_speed.png
│ └── powerup_sticky.png
├── src
├── gl_const.erl
└── wx_const.erl
└── test
├── breakout_test.exs
└── test_helper.exs
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 | ]
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where third-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | breakout-*.tar
24 |
25 | # Temporary files, for example, from tests.
26 | /tmp/
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Ian Harris
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Breakout
2 |
3 | Breakout in Elixir using OpenGL. Largely an incomplete and poor port of
4 | https://learnopengl.com/In-Practice/2D-Game/Breakout.
5 |
6 | ## Pre-emptive notes
7 |
8 | The code is very bad. I'm working on improving it. Please feel free to suggest
9 | updates/changes, ideally with an eye towards how games can be implemented in the
10 | future (i.e., not just improvements to this breakout implementation, but game
11 | development in Elixir as a whole).
12 |
13 | ## Launching
14 |
15 | This implementation has no dependencies. The following should work (tested on
16 | modern macOS, Ubuntu, and Windows):
17 |
18 | ```sh
19 | $ git clone https://github.com/harrisi/elixir_breakout
20 | $ iex -S mix # or mix run --no-halt
21 | ```
22 |
23 | ## Gameplay
24 |
25 | `A`/`D`, `H`/`L` (hello, fellow vim users), or left/right arrows to move
26 | left/right, space to launch the ball, `N` to switch between levels, `P` to
27 | profile with tprof for ten seconds (if you're in to that).
28 |
29 | There is no win condition.
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | config :logger,
4 | :default_formatter,
5 | format: {Breakout.Logger, :format},
6 | handle_otp_reports: true,
7 | handle_sasl_reports: true,
8 | # format: "$time [$level] $message\n\t$metadata\n",
9 | metadata: [
10 | :error_code,
11 | :mfa,
12 | :line,
13 | :pid,
14 | :registered_name,
15 | :process_label,
16 | :crash_reason,
17 | :msg
18 | ]
19 |
--------------------------------------------------------------------------------
/data.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 | Adobe Photoshop CS6 (Windows)
7 | 2018-06-19T20:29:54+02:00
8 | 2020-04-23T12:12:01+02:00
9 | 2020-04-23T12:12:01+02:00
10 |
11 |
13 | image/png
14 |
15 |
17 | 3
18 | sRGB IEC61966-2.1
19 |
20 |
23 | xmp.iid:133477AC4A85EA11B6AEC9C5B8B52EE9
24 | xmp.did:133477AC4A85EA11B6AEC9C5B8B52EE9
25 | xmp.did:133477AC4A85EA11B6AEC9C5B8B52EE9
26 |
27 |
28 |
29 | created
30 | xmp.iid:133477AC4A85EA11B6AEC9C5B8B52EE9
31 | 2018-06-19T20:29:54+02:00
32 | Adobe Photoshop CS6 (Windows)
33 |
34 |
35 |
36 |
37 |
39 | 1
40 | 720000/10000
41 | 720000/10000
42 | 2
43 |
44 |
46 | 1
47 | 512
48 | 512
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 |
--------------------------------------------------------------------------------
/lib/breakout.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout do
2 | end
3 |
--------------------------------------------------------------------------------
/lib/breakout/application.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Application do
2 | require Logger
3 |
4 | # See https://hexdocs.pm/elixir/Application.html
5 | # for more information on OTP Applications
6 | @moduledoc false
7 |
8 | use Application
9 |
10 | @impl Application
11 | def start(_type, _args) do
12 | children = [
13 | # having ResourceManager as a separate app makes things difficult because
14 | # that means I need to share the context. I should probably see what it
15 | # would look like to move all opengl stuff to it's own application
16 | # (Renderer), but I just realized that gets kinda complicated - what about
17 | # the window? should the renderer also handle input? hm.
18 |
19 | # for now, I'm actually going to just have a single app. it makes handling
20 | # opengl stuff easier.
21 |
22 | # Breakout.ResourceManager,
23 | {Breakout.Audio, [Breakout.Util.to_priv("audio/breakout.mp3")]},
24 | Breakout.Game,
25 | ]
26 |
27 | # See https://hexdocs.pm/elixir/Supervisor.html
28 | # for other strategies and supported options
29 | opts = [strategy: :one_for_one, name: Breakout.Supervisor, auto_shutdown: :any_significant]
30 | Supervisor.start_link(children, opts)
31 | end
32 |
33 | @impl Application
34 | def stop(state) do
35 | IO.inspect(state, label: "in application stop")
36 |
37 | :init.stop()
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/breakout/audio.ex:
--------------------------------------------------------------------------------
1 | # This is taken from one of the Membrane maintainers (Mateusz Front, I believe).
2 | # slight modification to make it loop infinitely.
3 | defmodule Breakout.Audio.LoopFilter do
4 | use Membrane.Filter
5 |
6 | def_input_pad :input, accepted_format: _any
7 | def_output_pad :output, accepted_format: _any
8 |
9 | def_options loops: [spec: integer() | :infinity]
10 |
11 | @impl true
12 | def handle_init(_ctx, opts) do
13 | {[], %{loops: opts.loops}}
14 | end
15 |
16 | @impl true
17 | def handle_playing(_ctx, state) do
18 | seek(state)
19 | end
20 |
21 | @impl true
22 | def handle_event(:input, %Membrane.File.EndOfSeekEvent{}, _ctx, state) do
23 | seek(state)
24 | end
25 |
26 | @impl true
27 | def handle_event(pad, event, ctx, state) do
28 | super(pad, event, ctx, state)
29 | end
30 |
31 | @impl true
32 | def handle_buffer(:input, buffer, _ctx, state) do
33 | {[buffer: {:output, buffer}], state}
34 | end
35 |
36 | defp seek(state) do
37 | %{loops: loops} = state
38 |
39 | event = case loops do
40 | :infinity ->
41 | %Membrane.File.SeekSourceEvent{start: :bof, size_to_read: :infinity}
42 |
43 | 1 ->
44 | %Membrane.File.SeekSourceEvent{start: :bof, size_to_read: :infinity, last?: true}
45 |
46 | _ -> nil
47 | end
48 |
49 | actions = if event, do: [event: {:input, event}], else: []
50 | state = unless state.loops == :infinity do
51 | update_in(state.loops, &(&1 - 1))
52 | else
53 | state
54 | end
55 |
56 | {actions, state}
57 | end
58 | end
59 |
60 | defmodule Breakout.Audio do
61 | use Membrane.Pipeline
62 |
63 | def start_link(args) do
64 | Membrane.Pipeline.start_link(__MODULE__, args, name: __MODULE__)
65 | end
66 |
67 | # This is taken from membraneframework/membrane_demo/simple_pipeline
68 |
69 | @impl Membrane.Pipeline
70 | def handle_init(_ctx, path_to_mp3) do
71 | spec =
72 | child(:file, %Membrane.File.Source{location: path_to_mp3, seekable?: true})
73 | |> child(:decoder, Membrane.MP3.MAD.Decoder)
74 | |> child(:converter, %Membrane.FFmpeg.SWResample.Converter{
75 | output_stream_format: %Membrane.RawAudio{
76 | sample_format: :s16le,
77 | sample_rate: 44_100,
78 | channels: 2,
79 | }
80 | })
81 | |> child(%Breakout.Audio.LoopFilter{loops: :infinity})
82 | |> child(:portaudio, Membrane.PortAudio.Sink)
83 |
84 | {[spec: spec], %{}}
85 | end
86 |
87 | @impl Membrane.Pipeline
88 | def handle_element_end_of_stream(:sink, :input, _ctx, state) do
89 | {[terminate: :normal], state}
90 | end
91 |
92 | @impl Membrane.Pipeline
93 | def handle_element_end_of_stream(_element, _pad, _ctx, state) do
94 | {[], state}
95 | end
96 |
97 | # @impl Membrane.Pipeline
98 | # def handle_element_end_of_stream(child, pad, context, state) do
99 | # Membrane.Pipeline.terminate(__MODULE__)
100 | # end
101 | end
102 |
103 | defmodule Breakout.Audio.SoundEffect do
104 | use Membrane.Pipeline
105 |
106 | def play(which) do
107 | case which do
108 | :block ->
109 | {:ok, _sup, _pipe} =
110 | Membrane.Pipeline.start(__MODULE__, Breakout.Util.to_priv("audio/block.mp3"))
111 | :solid ->
112 | {:ok, _sup, _pipe} =
113 | Membrane.Pipeline.start(__MODULE__, Breakout.Util.to_priv("audio/solid.mp3"))
114 | :paddle ->
115 | {:ok, _sup, _pipe} =
116 | Membrane.Pipeline.start(__MODULE__, Breakout.Util.to_priv("audio/paddle.mp3"))
117 | :powerup ->
118 | {:ok, _sup, _pipe} =
119 | Membrane.Pipeline.start(__MODULE__, Breakout.Util.to_priv("audio/powerup.mp3"))
120 | _ -> nil
121 | end
122 | end
123 |
124 | def start(args) do
125 | Membrane.Pipeline.start(__MODULE__, args, name: __MODULE__)
126 | end
127 |
128 | # This is taken from membraneframework/membrane_demo/simple_pipeline
129 |
130 | @impl Membrane.Pipeline
131 | def handle_init(_ctx, path_to_mp3) do
132 | spec =
133 | child(:file, %Membrane.File.Source{location: path_to_mp3})
134 | |> child(:decoder, Membrane.MP3.MAD.Decoder)
135 | |> child(:converter, %Membrane.FFmpeg.SWResample.Converter{
136 | output_stream_format: %Membrane.RawAudio{
137 | sample_format: :s16le,
138 | sample_rate: 48_000,
139 | channels: 2,
140 | }
141 | })
142 | # |> child(%Breakout.Audio.LoopFilter{loops: :infinity})
143 | |> child(:portaudio, Membrane.PortAudio.Sink)
144 |
145 | {[spec: spec], %{}}
146 | end
147 |
148 | @impl Membrane.Pipeline
149 | def handle_element_end_of_stream(:sink, :input, _ctx, state) do
150 | {[terminate: :normal], state}
151 | end
152 |
153 | @impl Membrane.Pipeline
154 | def handle_element_end_of_stream(_element, _pad, _ctx, state) do
155 | {[], state}
156 | end
157 | end
158 |
--------------------------------------------------------------------------------
/lib/breakout/ball_object.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.BallObject do
2 | alias Breakout.Math.Vec2
3 | alias Breakout.Math.Vec3
4 | alias Breakout.GameObject
5 |
6 | @type t :: %__MODULE__{
7 | game_object: GameObject.t(),
8 | radius: number(),
9 | stuck: boolean(),
10 | sticky: boolean(),
11 | passthrough: boolean()
12 | }
13 |
14 | defstruct game_object: GameObject.new(),
15 | radius: 1,
16 | stuck: true,
17 | sticky: false,
18 | passthrough: false
19 |
20 | def new() do
21 | %__MODULE__{}
22 | end
23 |
24 | def new(position, radius, velocity, sprite) do
25 | %__MODULE__{
26 | radius: radius,
27 | game_object:
28 | GameObject.new(
29 | position,
30 | Vec2.new(radius * 2, radius * 2),
31 | sprite,
32 | Vec3.new(1, 1, 1),
33 | velocity
34 | )
35 | }
36 | end
37 |
38 | @spec reset(ball :: t(), position :: Vec2.t(), velocity :: Vec2.t()) :: t()
39 | def reset(ball, position, velocity) do
40 | new(position, ball.radius, velocity, ball.game_object.sprite)
41 | end
42 |
43 | @spec move(ball :: t(), dt :: number(), window_width :: number()) :: t()
44 | def move(ball, dt, window_width) do
45 | unless ball.stuck do
46 | {this_velocity_x, this_velocity_y} = this_velocity = ball.game_object.velocity
47 | {this_size_x, _} = ball.game_object.size
48 |
49 | {new_x, new_y} =
50 | pos =
51 | ball.game_object.position
52 | |> Vec2.add(Vec2.scale(this_velocity, dt))
53 |
54 | {{new_velocity_x, new_velocity_y} = new_velocity,
55 | {new_position_x, new_position_y} = new_position} =
56 | cond do
57 | new_x <= 0 ->
58 | {{-this_velocity_x, this_velocity_y}, {0.0, new_y}}
59 |
60 | new_x + (ball.game_object.size |> elem(0)) >= window_width ->
61 | {{-this_velocity_x, this_velocity_y}, {window_width - this_size_x, new_y}}
62 |
63 | true ->
64 | {this_velocity, pos}
65 | end
66 |
67 | {new_velocity, new_position} =
68 | if new_position_y <= 0 do
69 | {{new_velocity_x, -new_velocity_y}, {new_position_x, 0.0}}
70 | else
71 | {new_velocity, new_position}
72 | end
73 |
74 | b = new(new_position, ball.radius, new_velocity, ball.game_object.sprite)
75 |
76 | # , radius: ball.radius, stuck: ball.stuck}
77 | %__MODULE__{ball | game_object: b.game_object}
78 | else
79 | ball
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/lib/breakout/game.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Game do
2 | require Logger
3 |
4 | import Breakout.WxRecords
5 |
6 | alias Breakout.State
7 | alias Breakout.PowerUp
8 | alias Breakout.Renderer.PostProcessor
9 | alias Breakout.ParticleGenerator
10 | alias Breakout.{BallObject, GameObject, GameLevel}
11 | alias Breakout.Renderer
12 | alias Renderer.{Texture2D, Sprite, Shader, Window, OpenGL}
13 | alias Breakout.Math
14 | alias Math.{Vec2, Vec3, Mat4}
15 | alias Breakout.Audio.SoundEffect
16 |
17 | @behaviour :wx_object
18 |
19 | @screen_width 1200
20 | @screen_height 800
21 |
22 | @initial_ball_velocity_x -0.25
23 | @initial_ball_velocity_y -0.25
24 | @initial_ball_velocity {@initial_ball_velocity_x, @initial_ball_velocity_y}
25 |
26 | @ball_radius 12.5
27 |
28 | @player_position_x @screen_width / 2 - 50
29 | @player_position_y @screen_height - 60
30 | @player_position {@player_position_x, @player_position_y}
31 |
32 | def child_spec(arg) do
33 | %{
34 | id: __MODULE__,
35 | start: {__MODULE__, :start_link, [arg]},
36 | significant: true,
37 | restart: :temporary
38 | }
39 | end
40 |
41 | @impl :wx_object
42 | def init(_arg) do
43 | window = Window.init(@screen_width, @screen_height)
44 |
45 | OpenGL.init()
46 |
47 | font = :wxFont.new(32, :wx_const.wx_fontfamily_teletype, :wx_const.wx_fontstyle_normal, :wx_const.wx_fontweight_normal)
48 | brush = :wxBrush.new({0, 0, 0})
49 |
50 | state = %State{
51 | t: :erlang.monotonic_time(:millisecond),
52 | start: :erlang.monotonic_time(),
53 | dt: 1,
54 | window: window,
55 | width: @screen_width,
56 | height: @screen_height,
57 | power_ups: [],
58 | ball: BallObject.new(),
59 | font: font,
60 | brush: brush,
61 | }
62 |
63 | menu_text = """
64 | press enter to start
65 | press w or s to select level
66 | """
67 |
68 | menu_font = :wxFont.new(60, :wx_const.wx_fontfamily_teletype, :wx_const.wx_fontstyle_normal, :wx_const.wx_fontweight_bold)
69 |
70 | {menu_string_texture, menu_string_w, menu_string_h} = load_texture_by_string(menu_font, brush, {222, 222, 222}, menu_text, false)
71 |
72 | state = put_in(state.menu_string_size, {menu_string_w, menu_string_h})
73 |
74 | state = put_in(state.resources.textures[:menu_string], menu_string_texture)
75 |
76 | projection = Mat4.ortho(0.0, state.width + 0.0, state.height + 0.0, 0.0, -1.0, 1.0)
77 |
78 | sprite_shader =
79 | Shader.init(Breakout.Util.to_priv("shaders/sprite/vertex.vs"), Breakout.Util.to_priv("shaders/sprite/fragment.fs"))
80 | |> Shader.use_shader()
81 | |> Shader.set(~c"image", 0)
82 | |> Shader.set(~c"projection", [projection |> Mat4.flatten()])
83 |
84 | # |> ResourceManager.put_shader(:sprite)
85 | # state = %Breakout.State{state | resources}
86 | state = put_in(state.resources.shaders[:sprite], sprite_shader)
87 | sprite_renderer = Sprite.new(sprite_shader)
88 |
89 | particle_shader =
90 | Shader.init(Breakout.Util.to_priv("shaders/particle/vertex.vs"), Breakout.Util.to_priv("shaders/particle/fragment.fs"))
91 | |> Shader.set(~c"projection", [projection |> Mat4.flatten()], true)
92 |
93 | state = put_in(state.resources.shaders[:particle], particle_shader)
94 |
95 | state =
96 | put_in(
97 | state.resources.textures[:particle],
98 | Texture2D.load(Breakout.Util.to_priv("textures/particle.png"), true)
99 | )
100 |
101 | state = %{
102 | state
103 | | particle_generator:
104 | ParticleGenerator.new(
105 | particle_shader,
106 | state.resources.textures[:particle],
107 | 500
108 | )
109 | }
110 |
111 | state = %Breakout.State{state | sprite_renderer: sprite_renderer}
112 |
113 | state =
114 | put_in(
115 | state.resources.textures[:face],
116 | Texture2D.load(Breakout.Util.to_priv("textures/awesomeface.png"), true)
117 | )
118 |
119 | state = put_in(state.resources.textures[:ascii],
120 | Texture2D.load(Breakout.Util.to_priv("textures/ascii_rgb.png"), false)
121 | )
122 |
123 | # |> ResourceManager.put_texture(:face)
124 |
125 | state =
126 | put_in(
127 | state.resources.textures[:background],
128 | Texture2D.load(Breakout.Util.to_priv("textures/background.png"), false)
129 | )
130 |
131 | state =
132 | put_in(state.resources.textures[:block], Texture2D.load(Breakout.Util.to_priv("textures/block.png"), false))
133 |
134 | power_up_textures =
135 | [
136 | :chaos,
137 | :confuse,
138 | :increase,
139 | :passthrough,
140 | :speed,
141 | :sticky
142 | ]
143 | |> Enum.reduce(state.resources.textures, fn el, acc ->
144 | put_in(acc[el], Texture2D.load(Breakout.Util.to_priv("textures/powerup_#{el}.png"), true))
145 | end)
146 |
147 | state = %Breakout.State{
148 | state
149 | | resources: %{
150 | state.resources
151 | | textures: Map.merge(state.resources.textures, power_up_textures)
152 | }
153 | }
154 |
155 | state =
156 | put_in(
157 | state.resources.textures[:block_solid],
158 | Texture2D.load(Breakout.Util.to_priv("textures/block_solid.png"), false)
159 | )
160 |
161 | # |> ResourceManager.put_texture(:block_solid)
162 |
163 | state = %Breakout.State{
164 | state
165 | | levels: {
166 | GameLevel.load(Breakout.Util.to_priv("levels/one.lvl"), @screen_width, @screen_height / 2),
167 | GameLevel.load(Breakout.Util.to_priv("levels/two.lvl"), @screen_width, @screen_height / 2),
168 | GameLevel.load(Breakout.Util.to_priv("levels/three.lvl"), @screen_width, @screen_height / 2),
169 | GameLevel.load(Breakout.Util.to_priv("levels/four.lvl"), @screen_width, @screen_height / 2)
170 | },
171 | level: 0
172 | }
173 |
174 | state =
175 | put_in(state.resources.textures[:paddle], Texture2D.load(Breakout.Util.to_priv("textures/paddle.png"), true))
176 |
177 | # |> ResourceManager.put_texture(:paddle)
178 |
179 | # {:ok, player_texture} = ResourceManager.get_texture(:paddle)
180 | player_texture = state.resources.textures[:paddle]
181 |
182 | player =
183 | GameObject.new(
184 | @player_position,
185 | Vec2.new(100, 20),
186 | player_texture,
187 | Vec3.new(1, 1, 1),
188 | 500.0
189 | )
190 |
191 | state = %Breakout.State{state | player: player}
192 |
193 | ball_pos =
194 | player.position
195 | |> Vec2.add(Vec2.new(@player_position_x / 2 - @ball_radius, -@ball_radius * 2))
196 |
197 | # {:ok, ball_tex} = ResourceManager.get_texture(:face)
198 | ball_tex = state.resources.textures[:face]
199 | ball = BallObject.new(ball_pos, @ball_radius, @initial_ball_velocity, ball_tex)
200 |
201 | state =
202 | %Breakout.State{state | ball: ball}
203 | |> reset_ball()
204 |
205 | # {:ok, background} = ResourceManager.get_texture(:background)
206 | background = state.resources.textures[:background]
207 |
208 | state = %Breakout.State{state | background_texture: background}
209 |
210 | pp_shader =
211 | Shader.init(
212 | Breakout.Util.to_priv("shaders/post_processor/vertex.vs"),
213 | Breakout.Util.to_priv("shaders/post_processor/fragment.fs")
214 | )
215 |
216 | {scaled_width, scaled_height} =
217 | Vec2.new(@screen_width, @screen_height)
218 | |> Vec2.scale(:wxWindow.getDPIScaleFactor(window.frame))
219 |
220 | post_processor = PostProcessor.new(pp_shader, trunc(scaled_width), trunc(scaled_height))
221 |
222 | state = %Breakout.State{state | post_processor: post_processor}
223 |
224 | send(self(), :loop)
225 |
226 | {window.frame, state}
227 | end
228 |
229 | @spec do_collisions(State.t()) :: State.t()
230 | def do_collisions(%State{} = state) do
231 | cur_level = state.levels |> elem(state.level)
232 |
233 | {updated_bricks, new_state} =
234 | Enum.map_reduce(cur_level.bricks, state, fn %GameObject{} = box, acc ->
235 | unless box.destroyed do
236 | {collided, dir, diff} = GameObject.check_collision(acc.ball, box)
237 |
238 | if collided do
239 | {box, new_state} =
240 | if not box.is_solid do
241 | new_box = %GameObject{box | destroyed: true}
242 | new_state = spawn_power_ups(acc, new_box)
243 | SoundEffect.play(:block)
244 |
245 | {new_box, new_state}
246 | else
247 | new_state = %State{
248 | acc
249 | | shake_time: 0.05,
250 | post_processor: %PostProcessor{acc.post_processor | shake: true}
251 | }
252 | SoundEffect.play(:solid)
253 |
254 | {box, new_state}
255 | end
256 |
257 | # TODO: this allows passing through solid blocks, which is kinda weird.
258 | updated_ball =
259 | unless new_state.ball.passthrough do
260 | resolve_collision(new_state.ball, dir, diff)
261 | else
262 | new_state.ball
263 | end
264 |
265 | new_state = %State{new_state | ball: updated_ball}
266 | {box, new_state}
267 | else
268 | {box, acc}
269 | end
270 | else
271 | {box, acc}
272 | end
273 | end)
274 |
275 | level = %{cur_level | bricks: updated_bricks}
276 |
277 | new_state =
278 | Enum.with_index(new_state.power_ups)
279 | |> Enum.reduce(new_state, fn {%PowerUp{game_object: %GameObject{}} = power_up, index},
280 | %State{} = acc ->
281 | unless power_up.game_object.destroyed do
282 | power_up =
283 | put_in(
284 | power_up.game_object.destroyed,
285 | power_up.game_object.position |> elem(1) >= @screen_height
286 | )
287 |
288 | if GameObject.check_collision(acc.player, power_up.game_object) do
289 | acc = activate_power_up(acc, power_up)
290 | power_up = put_in(power_up.game_object.destroyed, true)
291 | power_up = put_in(power_up.activated, true)
292 | power_ups = List.update_at(acc.power_ups, index, fn _ -> power_up end)
293 | SoundEffect.play(:powerup)
294 | put_in(acc.power_ups, power_ups)
295 | else
296 | power_ups = List.update_at(acc.power_ups, index, fn _ -> power_up end)
297 | put_in(acc.power_ups, power_ups)
298 | end
299 | else
300 | acc
301 | end
302 | end)
303 |
304 | {paddle_collision, _, _} = GameObject.check_collision(new_state.ball, state.player)
305 |
306 | ball =
307 | if not new_state.ball.stuck and paddle_collision do
308 | {player_x, _player_y} = new_state.player.position
309 | {player_w, _player_h} = new_state.player.size
310 | {ball_x, _} = new_state.ball.game_object.position
311 |
312 | center_board = player_x + player_w / 2
313 | distance = ball_x + new_state.ball.radius - center_board
314 | percentage = distance / (player_w / 2)
315 |
316 | strength = 2
317 | old_vel = new_state.ball.game_object.velocity
318 |
319 | ball = %{
320 | new_state.ball
321 | | game_object: %{
322 | new_state.ball.game_object
323 | | velocity:
324 | {@initial_ball_velocity_x * percentage * strength, @initial_ball_velocity_y}
325 | }
326 | }
327 |
328 | ball = %{
329 | ball
330 | | game_object: %{
331 | ball.game_object
332 | | velocity:
333 | Vec2.normalize(ball.game_object.velocity) |> Vec2.scale(Vec2.length(old_vel))
334 | }
335 | }
336 |
337 | {ball_vel_x, ball_vel_y} = ball.game_object.velocity
338 | ball = put_in(ball.game_object.velocity, {ball_vel_x, -1 * abs(ball_vel_y)})
339 |
340 | SoundEffect.play(:paddle)
341 |
342 | put_in(ball.stuck, ball.sticky)
343 | else
344 | new_state.ball
345 | end
346 |
347 | %State{new_state | levels: put_elem(state.levels, state.level, level), ball: ball}
348 | end
349 |
350 | defp resolve_collision(ball, dir, {diff_x, diff_y}) do
351 | {ball_vel_x, ball_vel_y} = ball.game_object.velocity
352 | {ball_pos_x, ball_pos_y} = ball.game_object.position
353 |
354 | case dir do
355 | :left ->
356 | ball = %{ball | game_object: %{ball.game_object | velocity: {-ball_vel_x, ball_vel_y}}}
357 | penetration = ball.radius - abs(diff_x)
358 |
359 | %{
360 | ball
361 | | game_object: %{ball.game_object | position: {ball_pos_x + penetration, ball_pos_y}}
362 | }
363 |
364 | :right ->
365 | ball = %{ball | game_object: %{ball.game_object | velocity: {-ball_vel_x, ball_vel_y}}}
366 | penetration = ball.radius - abs(diff_x)
367 |
368 | %{
369 | ball
370 | | game_object: %{ball.game_object | position: {ball_pos_x - penetration, ball_pos_y}}
371 | }
372 |
373 | :up ->
374 | ball = %{ball | game_object: %{ball.game_object | velocity: {ball_vel_x, -ball_vel_y}}}
375 | penetration = ball.radius - abs(diff_y)
376 |
377 | %{
378 | ball
379 | | game_object: %{ball.game_object | position: {ball_pos_x, ball_pos_y - penetration}}
380 | }
381 |
382 | :down ->
383 | ball = %{ball | game_object: %{ball.game_object | velocity: {ball_vel_x, -ball_vel_y}}}
384 | penetration = ball.radius - abs(diff_y)
385 |
386 | %{
387 | ball
388 | | game_object: %{ball.game_object | position: {ball_pos_x, ball_pos_y + penetration}}
389 | }
390 | end
391 | end
392 |
393 | def start do
394 | :wx_object.start_link(__MODULE__, [], [])
395 | end
396 |
397 | def start_link(arg) do
398 | :wx_object.start_link({:local, __MODULE__}, __MODULE__, arg, [])
399 | {:ok, self()}
400 | end
401 |
402 | @impl :wx_object
403 | def terminate(reason, state) do
404 | Logger.error(msg: reason)
405 | IO.inspect(reason, label: "terminate")
406 | Supervisor.stop(Breakout.Supervisor)
407 |
408 | {:shutdown, state}
409 | end
410 |
411 | @impl :wx_object
412 | def handle_event(wx(event: wxClose()), state) do
413 | IO.inspect(state, label: "closing")
414 | :wxWindow."Destroy"(state.window.frame)
415 |
416 | {:stop, :normal, state}
417 | end
418 |
419 | @impl :wx_object
420 | def handle_event(request, state) do
421 | IO.inspect(request, label: "handle_event")
422 | {:noreply, state}
423 | end
424 |
425 | @impl :wx_object
426 | def handle_call(request, _from, state) do
427 | IO.inspect(request, label: "handle_call")
428 | {:noreply, state}
429 | end
430 |
431 | @impl :wx_object
432 | def handle_cast({:key_down, key_code}, state) do
433 | state = %{
434 | state
435 | | keys: MapSet.put(state.keys, key_code),
436 | level:
437 | if(key_code == ?N,
438 | do: rem(state.level + 1, tuple_size(state.levels)),
439 | else: state.level
440 | )
441 | }
442 |
443 | if key_code == ?P do
444 | send(self(), :start_profiling)
445 | end
446 |
447 | {:noreply, state}
448 | end
449 |
450 | @impl :wx_object
451 | def handle_cast({:key_up, key_code}, state) do
452 | state = %{state | keys: MapSet.delete(state.keys, key_code)}
453 | state = %{state | keys_processed: MapSet.delete(state.keys_processed, key_code)}
454 |
455 | {:noreply, state}
456 | end
457 |
458 | @impl :wx_object
459 | def handle_cast(request, state) do
460 | IO.inspect(request, label: "handle_cast")
461 | {:noreply, state}
462 | end
463 |
464 | def handle_info(:start_profiling, state) do
465 | :tprof.start(%{type: :call_time})
466 | :tprof.enable_trace(:all)
467 | :tprof.set_pattern(:_, :_, :_)
468 | # :eprof.start_profiling([self()])
469 | # :eprof.log(~c'eprof')
470 | Process.send_after(self(), :stop_profiling, 10_000)
471 | {:noreply, state}
472 | end
473 |
474 | def handle_info(:stop_profiling, state) do
475 | :tprof.disable_trace(:all)
476 | sample = :tprof.collect()
477 | inspected = :tprof.inspect(sample, :process, :measurement)
478 | shell = :maps.get(self(), inspected)
479 |
480 | IO.puts(:tprof.format(shell))
481 |
482 | # :eprof.stop_profiling()
483 | # :eprof.analyze()
484 | {:noreply, state}
485 | end
486 |
487 | @impl :wx_object
488 | def handle_info(:loop, %State{} = state) do
489 | t = :erlang.monotonic_time(:millisecond)
490 | dt = t - state.t
491 | now = :erlang.monotonic_time()
492 | elapsed = now - state.start
493 | state = put_in(state.elapsed, elapsed / :erlang.convert_time_unit(1, :second, :native))
494 |
495 | send(self(), {:update, dt})
496 | send(self(), {:process_input, dt})
497 | send(self(), :render)
498 | send(self(), :loop)
499 |
500 | {:noreply, %State{state | t: t, dt: dt}}
501 | end
502 |
503 | @impl :wx_object
504 | def handle_info({:update, dt}, %State{} = state) do
505 | ball = BallObject.move(state.ball, dt, @screen_width)
506 |
507 | state = put_in(state.ball, ball)
508 |
509 | state = do_collisions(state)
510 |
511 | {_, ball_y} = state.ball.game_object.position
512 |
513 | state =
514 | if ball_y >= @screen_height do
515 | state = update_in(state.lives, &(&1 - 1))
516 | if state.lives == 0 do
517 | state = state
518 | |> reset_level()
519 | put_in(state.game_state, :menu)
520 | else
521 | state
522 | end
523 | |> reset_player()
524 | |> reset_ball()
525 | else
526 | state
527 | end
528 |
529 | state = update_power_ups(state, dt)
530 |
531 | pg =
532 | ParticleGenerator.update(
533 | state.particle_generator,
534 | dt,
535 | state.ball.game_object,
536 | 2,
537 | Vec2.new(state.ball.radius / 2.0, state.ball.radius / 2.0)
538 | )
539 |
540 | state = put_in(state.particle_generator, pg)
541 |
542 | {shake_time, pp} =
543 | if state.shake_time > 0 do
544 | st = state.shake_time - 0.005
545 |
546 | pp =
547 | if st <= 0 do
548 | %PostProcessor{state.post_processor | shake: false}
549 | else
550 | state.post_processor
551 | end
552 |
553 | {st, pp}
554 | else
555 | {state.shake_time, state.post_processor}
556 | end
557 |
558 | # t = :erlang.system_time() / 1_000_000_000
559 | # r = 127.5 * (1 + :math.sin(t))
560 | # g = 127.5 * (1 + :math.sin(t + 2 * :math.pi / 3))
561 | # b = 127.5 * (1 + :math.sin(t + 4 * :math.pi / 3))
562 |
563 | state = put_in(state.resources.textures[:string], load_texture_by_string(state.font, state.brush, {222, 222, 222}, "Lives: #{state.lives}", false) |> elem(0))
564 |
565 | {:noreply, %State{state | shake_time: shake_time, post_processor: pp}}
566 | end
567 |
568 | @impl :wx_object
569 | def handle_info({:process_input, dt}, state) do
570 | state = if state.game_state == :menu do
571 | state = if MapSet.member?(state.keys, 13) and not MapSet.member?(state.keys_processed, 13) do
572 | state = put_in(state.game_state, :active)
573 | put_in(state.keys_processed, MapSet.put(state.keys_processed, 13))
574 | else
575 | state
576 | end
577 | state = if MapSet.member?(state.keys, ?W) and not MapSet.member?(state.keys_processed, ?W) do
578 | state = update_in(state.level, &(rem(&1 + 1, tuple_size(state.levels))))
579 | put_in(state.keys_processed, MapSet.put(state.keys_processed, ?W))
580 | else
581 | state
582 | end
583 | if MapSet.member?(state.keys, ?S) and not MapSet.member?(state.keys_processed, ?S) do
584 | state = if state.level > 0 do
585 | update_in(state.level, &(&1 - 1))
586 | else
587 | put_in(state.level, 3)
588 | end
589 | put_in(state.keys_processed, MapSet.put(state.keys_processed, ?S))
590 | else
591 | state
592 | end
593 | else
594 | state
595 | end
596 |
597 | state =
598 | if state.game_state == :active do
599 | velocity = state.player.velocity * dt / 1_000
600 |
601 | state =
602 | if MapSet.member?(state.keys, ?A) or
603 | MapSet.member?(state.keys, ?H) or
604 | MapSet.member?(state.keys, 314) do
605 | {_, new_state} =
606 | get_and_update_in(state.player.position, fn {x, y} = orig ->
607 | if x >= 0 do
608 | {orig, {x - velocity, y}}
609 | else
610 | {orig, orig}
611 | end
612 | end)
613 |
614 | {player_x, _} = state.player.position
615 |
616 | new_ball =
617 | if player_x >= 0 and state.ball.stuck do
618 | {ball_x, ball_y} = state.ball.game_object.position
619 |
620 | b =
621 | BallObject.new(
622 | Vec2.new(ball_x - velocity, ball_y),
623 | state.ball.radius,
624 | state.ball.game_object.velocity,
625 | state.ball.game_object.sprite
626 | )
627 |
628 | %BallObject{
629 | state.ball
630 | | game_object: b.game_object,
631 | stuck: state.ball.stuck,
632 | radius: state.ball.radius
633 | }
634 | else
635 | state.ball
636 | end
637 |
638 | %Breakout.State{new_state | ball: new_ball}
639 | else
640 | state
641 | end
642 |
643 | state =
644 | if MapSet.member?(state.keys, ?D) or
645 | MapSet.member?(state.keys, ?L) or
646 | MapSet.member?(state.keys, 316) do
647 | {_, new_state} =
648 | get_and_update_in(state.player.position, fn {x, y} = orig ->
649 | if x + 100 <= @screen_width do
650 | {orig, {x + velocity, y}}
651 | else
652 | {orig, orig}
653 | end
654 | end)
655 |
656 | {player_x, _} = state.player.position
657 |
658 | new_ball =
659 | if player_x + 100 <= @screen_width and state.ball.stuck do
660 | {ball_x, ball_y} = state.ball.game_object.position
661 |
662 | b =
663 | BallObject.new(
664 | Vec2.new(ball_x + velocity, ball_y),
665 | state.ball.radius,
666 | state.ball.game_object.velocity,
667 | state.ball.game_object.sprite
668 | )
669 |
670 | %BallObject{
671 | state.ball
672 | | game_object: b.game_object,
673 | stuck: state.ball.stuck,
674 | radius: state.ball.radius
675 | }
676 | else
677 | state.ball
678 | end
679 |
680 | %Breakout.State{new_state | ball: new_ball}
681 | else
682 | state
683 | end
684 |
685 | state
686 | else
687 | state
688 | end
689 |
690 | state =
691 | if MapSet.member?(state.keys, ~c" " |> hd) do
692 | %Breakout.State{
693 | state
694 | | ball: %BallObject{
695 | state.ball
696 | | game_object: state.ball.game_object,
697 | radius: state.ball.radius,
698 | stuck: false
699 | }
700 | }
701 | else
702 | state
703 | end
704 |
705 | {:noreply, state}
706 | end
707 |
708 | def handle_info(:render, %Breakout.State{} = state) do
709 | :wx.batch(fn ->
710 | :gl.clearColor(0.0, 0.0, 0.0, 1.0)
711 | :gl.clear(:gl_const.gl_color_buffer_bit())
712 |
713 | if state.game_state in [:active, :menu] do
714 | PostProcessor.begin_render(state.post_processor)
715 |
716 | Sprite.draw(
717 | state,
718 | :background,
719 | Vec2.new(0, 0),
720 | Vec2.new(state.width, state.height),
721 | 0,
722 | Vec3.new(1, 1, 1)
723 | )
724 |
725 | level = state.levels |> elem(state.level)
726 | GameLevel.draw(level, state.sprite_renderer, state)
727 | GameObject.draw(state.player, :paddle, state.sprite_renderer, state)
728 | ParticleGenerator.draw(state.particle_generator)
729 | GameObject.draw(state.ball.game_object, :face, state.sprite_renderer, state)
730 |
731 | Enum.each(state.power_ups, fn %PowerUp{game_object: %GameObject{}} = power_up ->
732 | unless power_up.game_object.destroyed do
733 | GameObject.draw(power_up.game_object, power_up.type, state.sprite_renderer, state)
734 | end
735 | end)
736 |
737 | PostProcessor.end_render(state.post_processor)
738 | PostProcessor.render(state.post_processor, state.elapsed)
739 |
740 | Sprite.draw(
741 | state,
742 | :string,
743 | Vec2.new(10, 0),
744 | Vec2.new(200, 100),
745 | 0,
746 | Vec3.new(1, 1, 1)
747 | )
748 | end
749 |
750 | if state.game_state == :menu do
751 | {w, h} = state.menu_string_size
752 | Sprite.draw(
753 | state,
754 | :menu_string,
755 | # TODO: either I'm very tired, or this is.. weird.
756 | Vec2.new((state.width - w / 2) / 2, state.height / 2 - h),
757 | state.menu_string_size,
758 | 0,
759 | Vec3.new(1, 1, 1)
760 | )
761 | end
762 |
763 | :wxGLCanvas.swapBuffers(state.window.canvas)
764 | end)
765 |
766 | {:noreply, state}
767 | end
768 |
769 | @impl :wx_object
770 | def handle_info(info, state) do
771 | Logger.debug(info: info, state: state)
772 |
773 | {:noreply, state}
774 | end
775 |
776 | defp reset_level(%State{} = state) do
777 | levels =
778 | put_elem(
779 | state.levels,
780 | state.level,
781 | GameLevel.load(
782 | Breakout.Util.to_priv("levels/#{level_name(state.level)}"),
783 | @screen_width,
784 | @screen_height / 2
785 | )
786 | )
787 |
788 | state = put_in(state.levels, levels)
789 | put_in(state.lives, 3)
790 | end
791 |
792 | defp level_name(0), do: "one.lvl"
793 | defp level_name(1), do: "two.lvl"
794 | defp level_name(2), do: "three.lvl"
795 | defp level_name(3), do: "four.lvl"
796 |
797 | defp reset_player(%State{} = state) do
798 | player = %{state.player | position: @player_position, size: Vec2.new(100, 20)}
799 |
800 | put_in(state.player, player)
801 | end
802 |
803 | defp reset_ball(state) do
804 | ball =
805 | BallObject.reset(
806 | state.ball,
807 | Vec2.add(
808 | state.player.position,
809 | Vec2.new(100 / 2 - @ball_radius, -@ball_radius * 2)
810 | ),
811 | @initial_ball_velocity
812 | )
813 |
814 | put_in(state.ball, ball)
815 | end
816 |
817 | defp should_spawn(chance) do
818 | :rand.uniform(chance) == chance
819 | end
820 |
821 | @spec spawn_power_ups(state :: Breakout.State.t(), block :: GameObject.t()) ::
822 | Breakout.State.t()
823 | defp spawn_power_ups(state, block) do
824 | state
825 | |> maybe_spawn_power_up(:chaos, Vec3.new(0.9, 0.25, 0.25), 15, block, :chaos, 10)
826 | |> maybe_spawn_power_up(:confuse, Vec3.new(1, 0.3, 0.3), 15, block, :confuse, 10)
827 | |> maybe_spawn_power_up(:increase, Vec3.new(1, 0.6, 0.4), 10, block, :increase, 10)
828 | |> maybe_spawn_power_up(:passthrough, Vec3.new(0.5, 1, 0.5), 10, block, :passthrough, 10)
829 | |> maybe_spawn_power_up(:speed, Vec3.new(0.5, 0.5, 1), 10, block, :speed, 10)
830 | |> maybe_spawn_power_up(:sticky, Vec3.new(1, 0.5, 1), 10, block, :sticky, 10)
831 | end
832 |
833 | @spec maybe_spawn_power_up(
834 | state :: Breakout.State.t(),
835 | type :: PowerUp.power_up_types(),
836 | color :: Vec3.t(),
837 | duration :: number(),
838 | block :: GameObject.t(),
839 | texture :: atom(),
840 | chance :: non_neg_integer()
841 | ) :: Breakout.State.t()
842 | defp maybe_spawn_power_up(state, type, color, duration, %GameObject{} = block, texture, chance) do
843 | if should_spawn(chance) do
844 | power_up =
845 | PowerUp.new(
846 | type,
847 | color,
848 | duration + 0.0,
849 | block.position,
850 | state.resources.textures[texture]
851 | )
852 |
853 | Map.update(state, :power_ups, [power_up], &[power_up | &1])
854 | else
855 | state
856 | end
857 | end
858 |
859 | @spec activate_power_up(state :: State.t(), power_up :: PowerUp.t()) :: State.t()
860 | def activate_power_up(%State{} = state, %PowerUp{} = power_up) do
861 | case power_up.type do
862 | :speed ->
863 | update_in(state.ball.game_object.velocity, &Vec2.scale(&1, 1.2))
864 |
865 | :sticky ->
866 | %State{
867 | state
868 | | ball: %{state.ball | sticky: true},
869 | player: %{state.player | color: Vec3.new(1, 0.5, 1)}
870 | }
871 |
872 | :passthrough ->
873 | %State{
874 | state
875 | | ball: %{state.ball | passthrough: true}
876 | }
877 |
878 | :increase ->
879 | %State{
880 | state
881 | | player: %{
882 | state.player
883 | | size: {(state.player.size |> elem(0)) + 50, state.player.size |> elem(1)}
884 | }
885 | }
886 |
887 | :confuse ->
888 | unless state.post_processor.chaos do
889 | %State{
890 | state
891 | | post_processor: %{state.post_processor | confuse: true}
892 | }
893 | else
894 | state
895 | end
896 |
897 | :chaos ->
898 | unless state.post_processor.confuse do
899 | %State{
900 | state
901 | | post_processor: %{state.post_processor | chaos: true}
902 | }
903 | else
904 | state
905 | end
906 | _ ->
907 | state
908 | end
909 | end
910 |
911 | @spec update_power_ups(state :: State.t(), dt :: float()) :: State.t()
912 | defp update_power_ups(%State{} = state, dt) do
913 | state =
914 | Enum.with_index(state.power_ups)
915 | |> Enum.reduce(state, fn {%PowerUp{} = power_up, index}, %State{} = acc ->
916 | power_up =
917 | power_up.game_object.position
918 | |> update_in(&Vec2.add(&1, Vec2.scale(power_up.game_object.velocity, dt / 1_000)))
919 |
920 | if power_up.activated do
921 | # TODO: subtract some better amount of `dt` instead
922 | power_up = update_in(power_up.duration, &(&1 - dt / 500.0))
923 |
924 | power_up =
925 | if power_up.duration <= 0 do
926 | put_in(power_up.activated, false)
927 | else
928 | power_up
929 | end
930 |
931 | updated = List.update_at(acc.power_ups, index, fn _ -> power_up end)
932 |
933 | acc = put_in(acc.power_ups, updated)
934 |
935 | unless power_up.activated do
936 | case power_up.type do
937 | :sticky ->
938 | unless is_other_power_up_active(acc.power_ups, :sticky) do
939 | acc = put_in(acc.ball.sticky, false)
940 | put_in(acc.player.color, Vec3.new(1, 1, 1))
941 | else
942 | acc
943 | end
944 |
945 | :passthrough ->
946 | unless is_other_power_up_active(acc.power_ups, :passthrough) do
947 | acc = put_in(acc.ball.passthrough, false)
948 | put_in(acc.ball.game_object.color, Vec3.new(1, 1, 1))
949 | else
950 | acc
951 | end
952 |
953 | :confuse ->
954 | unless is_other_power_up_active(acc.power_ups, :confuse) do
955 | put_in(acc.post_processor.confuse, false)
956 | else
957 | acc
958 | end
959 |
960 | :chaos ->
961 | unless is_other_power_up_active(acc.power_ups, :chaos) do
962 | put_in(acc.post_processor.chaos, false)
963 | else
964 | acc
965 | end
966 |
967 | _ ->
968 | acc
969 | end
970 | else
971 | acc
972 | end
973 | else
974 | updated = List.update_at(acc.power_ups, index, fn _ -> power_up end)
975 |
976 | put_in(acc.power_ups, updated)
977 | end
978 | end)
979 |
980 | update_in(state.power_ups, fn el ->
981 | Enum.reject(el, fn %PowerUp{game_object: %GameObject{}} = power_up ->
982 | power_up.game_object.destroyed and not power_up.activated
983 | end)
984 | end)
985 | end
986 |
987 | defp is_other_power_up_active(power_ups, type) do
988 | power_ups
989 | |> Enum.any?(&(&1.activated and &1.type == type))
990 | end
991 |
992 | # This is taken from lib/wx/examples/demo/ex_gl.erl, with the following comment:
993 | # %% This algorithm (based on http://d0t.dbclan.de/snippets/gltext.html)
994 | # %% prints a string to a bitmap and loads that onto an opengl texture.
995 | # %% Comments for the createTexture function:
996 | # %%
997 | # %% "Creates a texture from the settings saved in TextElement, to be
998 | # %% able to use normal system fonts conviently a wx.MemoryDC is
999 | # %% used to draw on a wx.Bitmap. As wxwidgets device contexts don't
1000 | # %% support alpha at all it is necessary to apply a little hack to
1001 | # %% preserve antialiasing without sticking to a fixed background
1002 | # %% color:
1003 | # %%
1004 | # %% We draw the bmp in b/w mode so we can use its data as a alpha
1005 | # %% channel for a solid color bitmap which after GL_ALPHA_TEST and
1006 | # %% GL_BLEND will show a nicely antialiased text on any surface.
1007 | # %%
1008 | # %% To access the raw pixel data the bmp gets converted to a
1009 | # %% wx.Image. Now we just have to merge our foreground color with
1010 | # %% the alpha data we just created and push it all into a OpenGL
1011 | # %% texture and we are DONE *inhalesdelpy*"
1012 |
1013 | defp load_texture_by_string(font, brush, color, string, flip) do
1014 | tmp_bmp = :wxBitmap.new(200, 200)
1015 | tmp = :wxMemoryDC.new(tmp_bmp)
1016 | :wxMemoryDC.setFont(tmp, font)
1017 | {str_w, str_h} = :wxDC.getTextExtent(tmp, string)
1018 | :wxMemoryDC.destroy(tmp)
1019 | :wxBitmap.destroy(tmp_bmp)
1020 |
1021 | w = get_power_of_two_roof(str_w)
1022 | h = get_power_of_two_roof(str_h)
1023 |
1024 | bmp = :wxBitmap.new(w, h)
1025 | dc = :wxMemoryDC.new(bmp)
1026 | :wxMemoryDC.setFont(dc, font)
1027 | :wxMemoryDC.setBackground(dc, brush)
1028 | :wxMemoryDC.clear(dc)
1029 | :wxMemoryDC.setTextForeground(dc, {255, 255, 255})
1030 | :wxMemoryDC.drawText(dc, string, {0, 0})
1031 |
1032 | img_0 = :wxBitmap.convertToImage(bmp)
1033 | img = case flip do
1034 | true ->
1035 | img = :wxImage.mirror(img_0, horizontally: false)
1036 | :wxImage.destroy(img_0)
1037 | img
1038 | false ->
1039 | img_0
1040 | end
1041 |
1042 | alpha = :wxImage.getData(img)
1043 | data = colourize_image(alpha, color)
1044 | :wxImage.destroy(img)
1045 | :wxBitmap.destroy(bmp)
1046 | :wxMemoryDC.destroy(dc)
1047 |
1048 | [tid] = :gl.genTextures(1)
1049 | :gl.bindTexture(:gl_const.gl_texture_2d, tid)
1050 | :gl.texParameteri(:gl_const.gl_texture_2d, :gl_const.gl_texture_mag_filter, :gl_const.gl_linear)
1051 | :gl.texParameteri(:gl_const.gl_texture_2d, :gl_const.gl_texture_min_filter, :gl_const.gl_linear)
1052 | :gl.texEnvi(:gl_const.gl_texture_env, :gl_const.gl_texture_env_mode, :gl_const.gl_replace)
1053 | :gl.texImage2D(:gl_const.gl_texture_2d, 0, :gl_const.gl_rgba, w, h, 0, :gl_const.gl_rgba, :gl_const.gl_unsigned_byte, data)
1054 |
1055 | {%Texture2D{
1056 | id: tid,
1057 | width: w,
1058 | height: h,
1059 | internal_format: :gl_const.gl_rgba,
1060 | image_format: :gl_const.gl_rgba,
1061 | wrap_s: :gl_const.gl_repeat,
1062 | wrap_t: :gl_const.gl_repeat,
1063 | filter_min: :gl_const.gl_linear,
1064 | filter_max: :gl_const.gl_linear
1065 | }, str_w, str_h}
1066 | end
1067 |
1068 | defp colourize_image(alpha, {r, g, b}) do
1069 | for <>, into: <<>> do
1070 | <>
1071 | end
1072 | end
1073 |
1074 | defp get_power_of_two_roof(x), do: get_power_of_two_roof_2(1, x)
1075 | defp get_power_of_two_roof_2(n, x) when n >= x, do: n
1076 | defp get_power_of_two_roof_2(n, x), do: get_power_of_two_roof_2(n * 2, x)
1077 | end
1078 |
--------------------------------------------------------------------------------
/lib/breakout/game_level.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.GameLevel do
2 | # alias Breakout.ResourceManager
3 | alias Breakout.GameObject
4 | alias Breakout.Math.Vec3
5 |
6 | @type t :: %__MODULE__{
7 | bricks: [GameObject.t()]
8 | }
9 |
10 | defstruct bricks: []
11 |
12 | def new() do
13 | end
14 |
15 | def load(file, width, height) do
16 | data =
17 | File.stream!(file)
18 | |> Stream.map(&String.trim/1)
19 | |> Stream.map(&String.split/1)
20 | |> Stream.map(fn row ->
21 | Enum.map(row, &String.to_integer/1)
22 | end)
23 | |> Enum.into([])
24 |
25 | init(data, width, height)
26 | end
27 |
28 | def draw(level, renderer, state) do
29 | Enum.each(level.bricks, fn tile ->
30 | unless tile.destroyed, do: GameObject.draw(tile, tile.sprite, renderer, state)
31 | end)
32 | end
33 |
34 | def is_completed(level) do
35 | Enum.any?(level, fn tile ->
36 | not tile.is_solid && not tile.destroyed
37 | end)
38 | end
39 |
40 | defp init(tile_data, level_width, level_height) do
41 | height = length(tile_data)
42 | width = length(tile_data |> hd)
43 | unit_width = level_width / width
44 | unit_height = level_height / height
45 |
46 | bricks =
47 | for {row, y} <- Enum.with_index(tile_data),
48 | {tile, x} <- Enum.with_index(row),
49 | reduce: [] do
50 | acc ->
51 | pos = {unit_width * x, unit_height * y}
52 | size = {unit_width, unit_height}
53 |
54 | case tile do
55 | 1 ->
56 | # {:ok, texture} = ResourceManager.get_texture(:block_solid)
57 | obj = %GameObject{
58 | # TODO: I need to reorganize ResourceManager
59 | position: pos,
60 | size: size,
61 | sprite: :block_solid,
62 | color: Vec3.new(0.8, 0.8, 0.7),
63 | is_solid: true
64 | }
65 |
66 | [obj | acc]
67 |
68 | t when t > 1 ->
69 | color =
70 | case t do
71 | 2 -> Vec3.new(0.2, 0.6, 1.0)
72 | 3 -> Vec3.new(0, 0.7, 0)
73 | 4 -> Vec3.new(0.8, 0.8, 0.4)
74 | 5 -> Vec3.new(1, 0.5, 0)
75 | end
76 |
77 | # {:ok, texture} = ResourceManager.get_texture(:block)
78 | obj = %GameObject{
79 | position: pos,
80 | size: size,
81 | sprite: :block,
82 | color: color,
83 | # this is the default, but just to be explicit
84 | is_solid: false
85 | }
86 |
87 | [obj | acc]
88 |
89 | _ ->
90 | acc
91 | end
92 | end
93 |
94 | %__MODULE__{bricks: bricks}
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/lib/breakout/game_object.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.GameObject do
2 | alias Breakout.Renderer.Sprite
3 | # alias Breakout.Renderer.Texture2D
4 | alias Breakout.Math.Vec3
5 | alias Breakout.Math.Vec2
6 |
7 | @type t :: %__MODULE__{
8 | position: Vec2.t(),
9 | size: Vec2.t(),
10 | velocity: Vec2.t(),
11 | color: Vec3.t(),
12 | rotation: float(),
13 | is_solid: boolean(),
14 | destroyed: boolean(),
15 | sprite: atom()
16 | }
17 |
18 | defstruct position: Vec2.new(0, 0),
19 | size: Vec2.new(1, 1),
20 | velocity: Vec2.new(0, 0),
21 | color: Vec3.new(1, 1, 1),
22 | rotation: 0.0,
23 | is_solid: false,
24 | destroyed: false,
25 | sprite: nil
26 |
27 | def new() do
28 | %__MODULE__{}
29 | end
30 |
31 | def new(position, size, sprite, color, velocity) do
32 | %__MODULE__{
33 | position: position,
34 | size: size,
35 | sprite: sprite,
36 | color: color,
37 | velocity: velocity
38 | }
39 | end
40 |
41 | def draw(
42 | %__MODULE__{
43 | sprite: _sprite,
44 | position: position,
45 | size: size,
46 | rotation: rotation,
47 | color: color
48 | } = _game_object,
49 | name,
50 | _renderer,
51 | state
52 | ) do
53 | Sprite.draw(state, name, position, size, rotation, color)
54 | end
55 |
56 | def check_collision(%__MODULE__{position: {ax, ay}, size: {aw, ah}}, %__MODULE__{
57 | position: {bx, by},
58 | size: {bw, bh}
59 | }) do
60 | collision_x = ax + aw >= bx and bx + bw >= ax
61 | collision_y = ay + ah >= by and by + bh >= ay
62 |
63 | collision_x and collision_y
64 | end
65 |
66 | def check_collision(%{game_object: %{position: ball_position}, radius: radius}, %__MODULE__{
67 | size: {w, h},
68 | position: {x, y}
69 | }) do
70 | center = Vec2.add(ball_position, Vec2.new(radius, radius))
71 |
72 | {aabb_half_x, aabb_half_y} = aabb_half_extents = Vec2.new(w / 2, h / 2)
73 | aabb_center = Vec2.new(x + aabb_half_x, y + aabb_half_y)
74 |
75 | diff = Vec2.subtract(center, aabb_center)
76 |
77 | clamped = Vec2.clamp(diff, Vec2.scale(aabb_half_extents, -1.0), aabb_half_extents)
78 |
79 | closest = Vec2.add(aabb_center, clamped)
80 |
81 | diff = Vec2.subtract(closest, center)
82 |
83 | if Vec2.length(diff) < radius do
84 | {true, Vec2.direction(diff), diff}
85 | else
86 | {false, :up, Vec2.new(0, 0)}
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/lib/breakout/image_parser.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.ImageParser do
2 | require Logger
3 | def parse(data) do
4 | res = do_parse(data, %{})
5 | {:ok, res}
6 | end
7 |
8 | defp do_parse(<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, rest::binary>>, acc) do
9 | do_png_parse(rest, acc)
10 | |> decompress_png()
11 | |> reconstruct_png()
12 | end
13 |
14 | defp do_parse(<<0xFF, 0xD8, 0xFF, rest::binary>>, acc) do
15 | do_jpg_parse(rest, acc)
16 | end
17 |
18 | def do_jpg_parse(_rest, _acc) do
19 | end
20 |
21 | defp decompress_png(%{IDAT: data} = map) do
22 | Map.put(map, :IDAT, :zlib.uncompress(data))
23 | end
24 |
25 | defp paeth_predictor(a, b, c) do
26 | p = a + b - c
27 | pa = abs(p - a)
28 | pb = abs(p - b)
29 | pc = abs(p - c)
30 |
31 | cond do
32 | pa <= pb and pa <= pc -> a
33 | pb <= pc -> b
34 | true -> c
35 | end
36 | end
37 |
38 | defp recon_a(recon, _width, _height, _stride, bytes_per_pixel, i, c) do
39 | if c >= bytes_per_pixel do
40 | :binary.at(recon, i - bytes_per_pixel)
41 | else
42 | 0
43 | end
44 | end
45 |
46 | defp recon_b(recon, _width, _height, stride, _bytes_per_pixel, i, _c) do
47 | if i >= stride do
48 | :binary.at(recon, i - stride)
49 | else
50 | 0
51 | end
52 | end
53 |
54 | defp recon_c(recon, _width, _height, stride, bytes_per_pixel, i, c) do
55 | if i >= stride and c >= bytes_per_pixel do
56 | :binary.at(recon, i - stride - bytes_per_pixel)
57 | else
58 | 0
59 | end
60 | end
61 |
62 | defp reconstruct_png(%{IDAT: data, IHDR: %{width: width, height: height, type: type, depth: depth}} = map) do
63 | if width == 256 and height == 96, do: dbg(map)
64 | bytes_per_pixel = case type do
65 | 0 -> depth / 8
66 | 2 -> 3 * depth / 8
67 | 3 -> depth / 8
68 | 4 -> 4 * depth / 8
69 | 6 -> 4 * depth / 8
70 | _ -> raise "unknown type #{type}"
71 | end
72 | |> trunc()
73 |
74 | stride = width * bytes_per_pixel
75 |
76 | res = reconstruct_rows(data, width, height, stride, bytes_per_pixel, <<>>, 0)
77 | Map.put(map, :IDAT, res)
78 | end
79 |
80 | defp reconstruct_rows(<<>>, _width, _height, _stride, _bytes_per_pixel, recon, _i), do: recon
81 |
82 | defp reconstruct_rows(
83 | <>,
84 | width,
85 | height,
86 | stride,
87 | bytes_per_pixel,
88 | recon,
89 | i
90 | ) do
91 | {recon, rest, i} =
92 | 0..(stride - 1)
93 | |> Enum.reduce({recon, rest, i}, fn c, {recon_acc, rest_acc, i_acc} ->
94 | <> = rest_acc
95 |
96 | recon_x =
97 | case filter_type do
98 | 0 ->
99 | filt_x
100 |
101 | 1 ->
102 | filt_x + recon_a(recon_acc, width, height, stride, bytes_per_pixel, i_acc, c)
103 |
104 | 2 ->
105 | filt_x + recon_b(recon_acc, width, height, stride, bytes_per_pixel, i_acc, c)
106 |
107 | 3 ->
108 | filt_x +
109 | div(
110 | recon_a(recon_acc, width, height, stride, bytes_per_pixel, i_acc, c) +
111 | recon_b(recon_acc, width, height, stride, bytes_per_pixel, i_acc, c),
112 | 2
113 | )
114 |
115 | 4 ->
116 | filt_x +
117 | paeth_predictor(
118 | recon_a(recon_acc, width, height, stride, bytes_per_pixel, i_acc, c),
119 | recon_b(recon_acc, width, height, stride, bytes_per_pixel, i_acc, c),
120 | recon_c(recon_acc, width, height, stride, bytes_per_pixel, i_acc, c)
121 | )
122 |
123 | _ ->
124 | raise "unknown filter type: #{filter_type}"
125 | end
126 |
127 | {<>)>>, rest_acc, i_acc + 1}
128 | end)
129 |
130 | reconstruct_rows(rest, width, height, stride, bytes_per_pixel, recon, i)
131 | end
132 |
133 | defp do_png_parse(<>, acc) do
134 | my_crc = :erlang.crc32(<>)
135 |
136 | unless my_crc == crc do
137 | raise "Invalid crc"
138 | end
139 |
140 | acc =
141 | case do_png_chunk(<>, <>) do
142 | {:IDAT, data} -> Map.put(acc, :IDAT, Map.get(acc, :IDAT, <<>>) <> data)
143 | {key, vals} -> Map.put(acc, key, vals)
144 | _ -> acc
145 | end
146 |
147 | do_png_parse(rest, acc)
148 | end
149 |
150 | defp do_png_parse(<<>>, acc), do: acc
151 |
152 | defp do_png_chunk(
153 | "IHDR",
154 | <> =
155 | _data
156 | ) do
157 | {:IHDR,
158 | %{
159 | width: width,
160 | height: height,
161 | depth: depth,
162 | type: type,
163 | compression: compression,
164 | filter: filter,
165 | interlace: interlace
166 | }}
167 | end
168 |
169 | defp do_png_chunk("IDAT", data) do
170 | {:IDAT, data}
171 | end
172 |
173 | defp do_png_chunk("IEND", _) do
174 | :ok
175 | end
176 |
177 | defp do_png_chunk("sRGB", <>) do
178 | {:sRGB, %{intent: intent}}
179 | end
180 |
181 | defp do_png_chunk("pHYs", <>) do
182 | {:pHYs, %{x: x, y: y, unit: unit}}
183 | end
184 |
185 | defp do_png_chunk("tIME", <>) do
186 | {:tIME, %{time: NaiveDateTime.new(year, month, day, hour, minute, second)}}
187 | end
188 |
189 | defp do_png_chunk("tEXt", data) do
190 | [keyword, data] = :binary.split(data, <<0>>)
191 | {:tEXt, %{keyword: keyword, data: data}}
192 | end
193 |
194 | defp do_png_chunk("iTXt", data) do
195 | [keyword, data] = :binary.split(data, <<0>>)
196 | <> = data
197 |
198 | [lang_tag, data] = :binary.split(data, <<0>>)
199 | [translated_keyword, data] = :binary.split(data, <<0>>)
200 |
201 | xml = :xmerl_scan.string(:binary.bin_to_list(data))
202 |
203 | {:iTXt,
204 | %{
205 | keyword: keyword,
206 | data: xml,
207 | compression_flag: compression_flag,
208 | compression_method: compression_method,
209 | lang_tag: lang_tag,
210 | translated_keyword: translated_keyword
211 | }}
212 | end
213 |
214 | defp do_png_chunk("eXIf", data) do
215 | {:eXIf, data}
216 | end
217 |
218 | defp do_png_chunk("iCCP", data) do
219 | [profile_name, data] = :binary.split(data, <<0>>)
220 |
221 | <> = data
222 |
223 | uncompressed = :zlib.uncompress(compressed_profile)
224 |
225 | # File.write("uncompressed", uncompressed)
226 | <> = uncompressed
227 |
228 | <<
229 | profile_size::unsigned-size(4)-unit(8), # 0-3
230 | preferred_cmm_type::binary-size(4), # 4-7
231 | # profile_version_raw::binary-size(2), # 8-11
232 | 0::size(4),
233 | profile_version_major::size(4),
234 | profile_version_minor::size(4),
235 | 0::size(4),
236 | 0::unit(8)-size(2), # Lowest two are reserved
237 | profile_class::binary-size(4), # 12-15
238 | color_space::binary-size(4), # 16-20
239 | pcs::binary-size(4), # 20-23
240 | creation_date_time::binary-size(12), # 24-35
241 | "acsp", # 36-39
242 | primary_platform_signature::binary-size(4), # 40-43
243 | # profile_flags::binary-size(4), # 44-47
244 | flag_embedded::size(1),
245 | flag_independent::size(1),
246 | 0::size(30),
247 | device_manufacturer::binary-size(4), # 48-51
248 | device_model::binary-size(4), # 52-55
249 | # device_attributes::binary-size(8), # 56-63 (really, 56-59)
250 | # TODO: these should probably be something like
251 | # attributes: %{reflective: true, glossy: false, ...}
252 | # or something. kind of awkward since they're mutually exclusive.
253 | reflective_or_transparency::size(1),
254 | glossy_or_matte::size(1),
255 | positive_or_negative_polarity::size(1),
256 | color_or_bw::size(1),
257 | 0::size(28),
258 | device_attributes::binary-size(4), # really, 60-63
259 | rendering_intent::binary-size(4), # 64-67
260 | nciexyz::binary-size(12), # 68-79
261 | signature::binary-size(4), # 80-83
262 | profile_id::binary-size(16), # 84-99
263 | 0::unit(8)-size(28) # 100-127
264 | >> = header
265 |
266 | profile_version = "#{profile_version_major}.#{profile_version_minor}"
267 |
268 | iccp = %{
269 | profile_name: profile_name,
270 | compression_method: compression_method,
271 | compressed_profile: compressed_profile,
272 | uncompressed: uncompressed,
273 | profile_size: profile_size,
274 | preferred_cmm_type: preferred_cmm_type,
275 | profile_version_major: profile_version_major,
276 | profile_version_minor: profile_version_minor,
277 | profile_version: profile_version,
278 | profile_class_raw: profile_class,
279 | profile_class: case profile_class do
280 | "mntr" -> :monitor
281 | "scnr" -> :scanner
282 | "prtr" -> :printer
283 | "link" -> :link
284 | "abst" -> :abstract
285 | "spac" -> :space
286 | "nmcl" -> :named_color
287 | _ -> :unknown
288 | end,
289 | color_space_raw: color_space,
290 | color_space: case color_space do
291 | "XYZ " -> [:nciexyz, :pcsxyz]
292 | "Lab " -> [:cielab, :pcslab]
293 | "Luv " -> :cieluv
294 | "YCbr" -> :ycbcr
295 | "Yxy " -> :cieyxy
296 | "RGB " -> :rgb
297 | "GRAY" -> :gray
298 | "HSV " -> :hsv
299 | "HLS " -> :hls
300 | "CMYK" -> :cmyk
301 | "CMY " -> :cmy
302 | # 2-15 color
303 | <> when digit in ?0..?9//1 ->
304 | String.to_atom("color_#{[digit]}")
305 | <> when digit in ?A..?F//1 ->
306 | String.to_atom("color_#{digit - ?A + 10}")
307 | end,
308 | pcs_raw: pcs,
309 | # TODO: kind of awkward to duplicate this.
310 | pcs: case color_space do
311 | # oh, this is where I decide (n)cie/pcs, I think?
312 | "XYZ " -> [:nciexyz, :pcsxyz]
313 | "Lab " -> [:cielab, :pcslab]
314 | _ -> :unknown
315 | end,
316 | creation_date_time_raw: creation_date_time,
317 | creation_date_time: parse_iccp_date_time(creation_date_time),
318 | primary_platform_signature_raw: primary_platform_signature,
319 | primary_platform_signature: case primary_platform_signature do
320 | "MSFT" -> :microsoft
321 | "APPL" -> :apple
322 | "ADBE" -> :adobe # this isn't specified in icc.1:2022, but is in some random other icc profile parser (icc node package)
323 | "SUNW" -> :sun_microsystems
324 | "SGI " -> :silicon_graphics
325 | "TGNT" -> :taligent # this isn't specified in icc.1:2022, but is in some random other icc profile parser (icc node package)
326 | _ -> :unknown
327 | end,
328 | profile_flags: %{
329 | embedded: flag_embedded,
330 | independent: flag_independent,
331 | },
332 | device_manufacturer: device_manufacturer,
333 | device_model: device_model,
334 | device_attributes: device_attributes,
335 | reflective_or_transparency: reflective_or_transparency,
336 | glossy_or_matte: glossy_or_matte,
337 | positive_or_negative_polarity: positive_or_negative_polarity,
338 | color_or_bw: color_or_bw,
339 | rendering_intent: rendering_intent,
340 | nciexyz: nciexyz,
341 | signature: signature,
342 | profile_id: profile_id,
343 | # TODO: This is just a list of pointers to the rest of the data
344 | tag_table: parse_tag_table(tag_table),
345 | }
346 |
347 | {:iCCP, iccp}
348 | end
349 |
350 | defp do_png_chunk("cHRM", data) do
351 | <<
352 | white_x::32,
353 | white_y::32,
354 | red_x::32,
355 | red_y::32,
356 | green_x::32,
357 | green_y::32,
358 | blue_x::32,
359 | blue_y::32
360 | >> = data
361 |
362 | chrm = %{
363 | white_x: white_x / 100_000,
364 | white_y: white_y / 100_000,
365 | red_x: red_x / 100_000,
366 | red_y: red_y / 100_000,
367 | green_x: green_x / 100_000,
368 | green_y: green_y / 100_000,
369 | blue_x: blue_x / 100_000,
370 | blue_y: blue_y / 100_000,
371 | }
372 |
373 | {:cHRM, chrm}
374 | end
375 |
376 | defp do_png_chunk(type, data) do
377 | IO.puts("unknown type #{type}")
378 | {String.to_atom(type), %{data: data}}
379 | end
380 |
381 | defp parse_iccp_date_time(<>) do
382 | DateTime.new(Date.new!(year, month, day), Time.new!(hours, minutes, seconds))
383 | end
384 |
385 | defp parse_tag_table(tag_table) do
386 | # File.write("tag_table_raw", tag_table)
387 | # <> = tag_table
388 | <> = tag_table
389 |
390 | table = do_parse_tag_table(tag_table, [], count)
391 |
392 | # File.write("tag_table", tag_table)
393 |
394 | <<_tag_table::binary-size(count * 12), data::binary>> = tag_table
395 |
396 | # File.write("data", data)
397 |
398 | first_offset = table |> hd() |> elem(1)
399 | table = Enum.map(table, fn {sig, offset, size} ->
400 | {sig, offset - first_offset, size}
401 | end)
402 |
403 | tagged_data = do_parse_tagged_data(table, data, %{})
404 |
405 | {table, tagged_data}
406 |
407 | # Enum.reduce(tag_table, fn <> = el, acc ->
408 | # IO.inspect({signature, offset, size}, label: "tag")
409 | # [el | acc]
410 | # end)
411 | end
412 |
413 | defp do_parse_tagged_data([], _data, acc) do
414 | acc
415 | end
416 |
417 | defp do_parse_tagged_data(_table, <<>>, acc) do
418 | # this is happening for every image it seems, so just mute the warning for now
419 | # Logger.warning("tag table indicates there's more data, but no more data exists in the tagged element data", msg: table)
420 | acc
421 | end
422 |
423 | defp do_parse_tagged_data(table, <<0::8, data::binary>>, acc) do
424 | do_parse_tagged_data(table, data, acc)
425 | end
426 |
427 | defp do_parse_tagged_data([{sig, _offset, size} | rest], data, acc) do
428 | <> = data
429 | parsed = do_parse_signature(to_parse)
430 | acc = put_in(acc[sig], parsed)
431 | do_parse_tagged_data(rest, next, acc)
432 | end
433 |
434 | defp do_parse_signature(<<"text", 0::32, text::binary>>) do
435 | text
436 | |> :binary.part(0, byte_size(text) - 1)
437 | end
438 |
439 | defp do_parse_signature(<<"desc", 0::32, rest::binary>>) do
440 | # from what I can tell.. this is like..
441 | # 0::32, length::32, data::size(length),
442 | # then.. 0::8, 0::8, length::32, repeat?
443 | # <> = rest
444 | do_parse_desc(rest, [])
445 | end
446 |
447 | defp do_parse_signature(<<"XYZ ", 0::32, x_sign::16, x::signed-16, y_sign::16, y::16, z_sign::16, z::16>>) do
448 | # TODO: this is wrong, but the spec is pretty unclear about how this is supposed to work.
449 | {x_sign + x, y_sign + y, z_sign + z}
450 | end
451 |
452 | defp do_parse_signature(<<"view", 0::32, illuminant::binary-size(12), surround::binary-size(12), type::binary>>) do
453 | {illuminant, surround, type}
454 | end
455 |
456 | defp do_parse_signature(<<"meas", 0::32, observer::32, tristimulus::binary-size(12), geo::32, flare::32, illuminant::32>>) do
457 | {observer, tristimulus, geo, flare, illuminant}
458 | end
459 |
460 | defp do_parse_signature(<<"sig ", 0::32, sig::binary>>) do
461 | sig
462 | end
463 |
464 | defp do_parse_signature(<<"curv", 0::32, count::32, curves::binary>>) do
465 | {count, curves}
466 | end
467 |
468 | defp do_parse_signature(<>) do
469 | Logger.warning("unknown signature #{data}")
470 |
471 | # continue parsing, why not
472 | rest
473 | end
474 |
475 | defp do_parse_desc(<<>>, acc), do: Enum.reverse(acc)
476 |
477 | defp do_parse_desc(<<0::32, rest::binary>>, acc) do
478 | # TODO: this is hacky. I'm not sure why it ends with a single null byte.
479 | if byte_size(rest) < 4 do
480 | do_parse_desc(<<>>, acc)
481 | else
482 | do_parse_desc(rest, acc)
483 | end
484 | end
485 |
486 | defp do_parse_desc(data, acc) do
487 | <> = data
488 |
489 | <> = next
490 | do_parse_desc(next, [res | acc])
491 | end
492 |
493 | defp do_parse_tag_table(_data, acc, 0), do: Enum.reverse(acc)
494 | defp do_parse_tag_table(<<>>, _acc, _count), do: raise "premature end of data"
495 |
496 | defp do_parse_tag_table(<>, acc, count) do
497 | do_parse_tag_table(next, [{signature, offset, size} | acc], count - 1)
498 | end
499 | end
500 | defmodule BCD do
501 | def decode(val) do
502 | Integer.undigits(for <>, do: x)
503 | end
504 | end
505 |
--------------------------------------------------------------------------------
/lib/breakout/input.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Input do
2 | require Logger
3 |
4 | import Breakout.WxRecords
5 |
6 | # we can just match on type, but matching on the specific mouse event type
7 | # (:motion, :left_down, :middle_dclick, etc.) lets us be more specific.
8 | # we need to connect the callback in Breakout.Window.
9 | def handler(wx(event: wxMouse(type: :motion, x: _x, y: _y)), state) do
10 | {:noreply, state}
11 | end
12 |
13 | def handler(wx(event: wxKey(type: type, x: _x, y: _y, keyCode: key_code)) = _request, state) do
14 | # IO.inspect(key_code, label: type)
15 | # send(Breakout.Game, {type, key_code})
16 | :wx_object.cast(Breakout.Game, {type, key_code})
17 |
18 | {:noreply, state}
19 | end
20 |
21 | def handler(request, state) do
22 | Logger.debug(request: request, state: state)
23 |
24 | {:noreply, state}
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/breakout/logger.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Logger do
2 | def format(level, message, {_date, time} = _timestamp, metadata) do
3 | # \t#{inspect(metadata)}\n"
4 | msg = "\n[#{level}] #{Logger.Formatter.format_time(time)}: #{message}\n\t"
5 |
6 | meta =
7 | for {k, v} <- metadata do
8 | "#{inspect(k)}, #{inspect(v)}"
9 | end
10 | |> Enum.join("\n\t")
11 |
12 | msg <> meta <> "\n"
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/breakout/math/mat2.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Math.Mat2 do
2 | alias Breakout.Math.Vec2
3 |
4 | @type t :: {Vec2.t(), Vec2.t()}
5 |
6 | @spec flatten(matrix :: t()) ::
7 | {float(), float(), float(), float()}
8 | def flatten({{a0, a1}, {b0, b1}}) do
9 | {a0, a1, b0, b1}
10 | end
11 |
12 | @spec scale(mat :: t(), scalar :: number()) :: t()
13 | def scale({r0, r1}, scalar) do
14 | {Vec2.scale(r0, scalar), Vec2.scale(r1, scalar)}
15 | end
16 |
17 | @spec add(mat1 :: t(), mat2 :: t()) :: t()
18 | def add({r0, r1}, {s0, s1}) do
19 | {Vec2.add(r0, s0), Vec2.add(r1, s1)}
20 | end
21 |
22 | @spec determinant(mat :: t()) :: float()
23 | def determinant({{r0x, r0y}, {r1x, r1y}}) do
24 | r0x * r1y - r0y * r1x
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/breakout/math/mat3.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Math.Mat3 do
2 | alias Breakout.Math.Mat2
3 | alias Breakout.Math.Vec3
4 |
5 | @type t :: {Vec3.t(), Vec3.t(), Vec3.t()}
6 |
7 | @spec flatten(matrix :: t()) ::
8 | {float(), float(), float(), float(), float(), float(), float(), float(), float()}
9 | def flatten({{a0, a1, a2}, {b0, b1, b2}, {c0, c1, c2}}) do
10 | {a0, a1, a2, b0, b1, b2, c0, c1, c2}
11 | end
12 |
13 | @spec zero() :: t()
14 | def zero() do
15 | {
16 | Vec3.new(0, 0, 0),
17 | Vec3.new(0, 0, 0),
18 | Vec3.new(0, 0, 0)
19 | }
20 | end
21 |
22 | @spec identity() :: t()
23 | def identity() do
24 | {
25 | Vec3.new(1, 0, 0),
26 | Vec3.new(0, 1, 0),
27 | Vec3.new(0, 0, 1)
28 | }
29 | end
30 |
31 | @spec scale(mat :: t(), scalar :: number()) :: t()
32 | def scale({r0, r1, r2}, scalar) do
33 | {
34 | Vec3.scale(r0, scalar),
35 | Vec3.scale(r1, scalar),
36 | Vec3.scale(r2, scalar)
37 | }
38 | end
39 |
40 | @spec add(mat1 :: t(), mat2 :: t()) :: t()
41 | def add({r0, r1, r2}, {s0, s1, s2}) do
42 | {
43 | Vec3.add(r0, s0),
44 | Vec3.add(r1, s1),
45 | Vec3.add(r2, s2)
46 | }
47 | end
48 |
49 | @spec trace(mat :: t()) :: float()
50 | def trace({{r0x, _, _}, {_, r1y, _}, {_, _, r2z}}) do
51 | r0x * r0x + r1y * r1y + r2z * r2z
52 | end
53 |
54 | @spec determinant(mat :: t()) :: float()
55 | def determinant({{r0x, r0y, r0z}, {r1x, r1y, r1z}, {r2x, r2y, r2z}}) do
56 | i = r0x * (r1y * r2z - r1z * r2y)
57 | j = r0y * (r1x * r2z - r1z * r2x)
58 | k = r0z * (r1x * r2y - r1y * r2x)
59 |
60 | i - j + k
61 | end
62 |
63 | @spec transpose(mat :: t()) :: t()
64 | def transpose({
65 | {r0x, r0y, r0z},
66 | {r1x, r1y, r1z},
67 | {r2x, r2y, r2z}
68 | }) do
69 | {
70 | {r0x, r1x, r2x},
71 | {r0y, r1y, r2y},
72 | {r0z, r1z, r2z}
73 | }
74 | end
75 |
76 | @spec inverse(mat :: t()) :: t()
77 | def inverse(m) do
78 | inv = {
79 | {cofactor(m, 0, 0), cofactor(m, 1, 0), cofactor(m, 2, 0)},
80 | {cofactor(m, 0, 1), cofactor(m, 1, 1), cofactor(m, 2, 1)},
81 | {cofactor(m, 0, 2), cofactor(m, 1, 2), cofactor(m, 2, 2)}
82 | }
83 |
84 | inv_det = 1 / determinant(m)
85 |
86 | scale(inv, inv_det)
87 | end
88 |
89 | @spec minor(mat :: t(), i :: integer(), j :: integer()) :: Mat2.t()
90 | def minor(mat, i, j) do
91 | rows = Tuple.to_list(mat)
92 |
93 | minor_rows =
94 | rows
95 | |> Enum.with_index()
96 | |> Enum.reject(fn {_row, y} -> y == j end)
97 | |> Enum.map(fn {row, _y} ->
98 | row
99 | |> Tuple.to_list()
100 | |> Enum.with_index()
101 | |> Enum.reject(fn {_value, x} -> x == i end)
102 | |> Enum.map(&elem(&1, 0))
103 | |> List.to_tuple()
104 | end)
105 |
106 | List.to_tuple(minor_rows)
107 | end
108 |
109 | @spec cofactor(mat :: t(), i :: integer(), j :: integer()) :: float()
110 | def cofactor(mat, i, j) do
111 | :math.pow(-1, i + 1 + j + 1) * Mat2.determinant(minor(mat, i, j))
112 | end
113 |
114 | @spec to_binary(matrix :: t()) :: binary()
115 | def to_binary({a, b, c}) do
116 | Vec3.to_binary(a) <> Vec3.to_binary(b) <> Vec3.to_binary(c)
117 | end
118 | end
119 |
--------------------------------------------------------------------------------
/lib/breakout/math/mat4.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Math.Mat4 do
2 | alias Breakout.Math.Mat3
3 | alias Breakout.Math.Vec3
4 | alias Breakout.Math.Vec4
5 |
6 | @type t :: {Vec4.t(), Vec4.t(), Vec4.t(), Vec4.t()}
7 |
8 | @spec flatten(matrix :: t()) ::
9 | {float(), float(), float(), float(), float(), float(), float(), float(), float(),
10 | float(), float(), float(), float(), float(), float(), float()}
11 | def flatten({{a0, a1, a2, a3}, {b0, b1, b2, b3}, {c0, c1, c2, c3}, {d0, d1, d2, d3}}) do
12 | {a0, a1, a2, a3, b0, b1, b2, b3, c0, c1, c2, c3, d0, d1, d2, d3}
13 | end
14 |
15 | @spec zero() :: t()
16 | def zero() do
17 | {
18 | Vec4.new(0, 0, 0, 0),
19 | Vec4.new(0, 0, 0, 0),
20 | Vec4.new(0, 0, 0, 0),
21 | Vec4.new(0, 0, 0, 0)
22 | }
23 | end
24 |
25 | @spec identity() :: t()
26 | def identity() do
27 | {
28 | Vec4.new(1, 0, 0, 0),
29 | Vec4.new(0, 1, 0, 0),
30 | Vec4.new(0, 0, 1, 0),
31 | Vec4.new(0, 0, 0, 1)
32 | }
33 | end
34 |
35 | @spec trace(mat :: t()) :: float()
36 | def trace({{r0x, _, _, _}, {_, r1y, _, _}, {_, _, r2z, _}, {_, _, _, r3w}}) do
37 | r0x * r0x + r1y * r1y + r2z * r2z + r3w * r3w
38 | end
39 |
40 | @spec determinant(mat :: t()) :: float()
41 | def determinant(mat) do
42 | Enum.reduce(0..3, 0, fn j, acc ->
43 | minor_mat = minor(mat, 0, j)
44 | cofactor = elem(mat, 0) |> elem(j)
45 | sign = if rem(j, 2) == 0, do: 1, else: -1
46 | acc + cofactor * Mat3.determinant(minor_mat) * sign
47 | end)
48 | end
49 |
50 | @spec transpose(mat :: t()) :: t()
51 | def transpose(mat) do
52 | mat
53 | |> Tuple.to_list()
54 | |> Enum.map(&Tuple.to_list/1)
55 | |> Enum.zip()
56 | |> List.to_tuple()
57 | end
58 |
59 | @spec inverse(mat :: t()) :: t()
60 | def inverse(mat) do
61 | inv =
62 | for i <- 0..3 do
63 | for j <- 0..3 do
64 | cofactor(mat, i, j)
65 | end
66 | end
67 | |> Enum.map(&List.to_tuple/1)
68 | |> List.to_tuple()
69 |
70 | inv_det = 1 / determinant(mat)
71 |
72 | scale(inv, inv_det)
73 | end
74 |
75 | @spec minor(mat :: t(), i :: integer(), j :: integer()) :: Mat3.t()
76 | def minor(mat, i, j) do
77 | rows = Tuple.to_list(mat)
78 |
79 | minor_rows =
80 | rows
81 | |> Enum.with_index()
82 | |> Enum.reject(fn {_row, y} -> y == j end)
83 | |> Enum.map(fn {row, _y} ->
84 | row
85 | |> Tuple.to_list()
86 | |> Enum.with_index()
87 | |> Enum.reject(fn {_value, x} -> x == i end)
88 | |> Enum.map(&elem(&1, 0))
89 | |> List.to_tuple()
90 | end)
91 |
92 | List.to_tuple(minor_rows)
93 | end
94 |
95 | @spec cofactor(mat :: t(), i :: integer(), j :: integer()) :: float()
96 | def cofactor(mat, i, j) do
97 | :math.pow(-1, i + 1 + j + 1) * Mat3.determinant(minor(mat, i, j))
98 | end
99 |
100 | @spec orient(pos :: Vec3.t(), fwd :: Vec3.t(), up :: Vec3.t()) :: t()
101 | def orient({px, py, pz}, {fx, fy, fz} = fwd, {ux, uy, uz} = up) do
102 | {lx, ly, lz} = Vec3.cross(up, fwd)
103 |
104 | # this is for the physics coordinate system where
105 | # +x-axis = fwd
106 | # +y-axis = left
107 | # +z-axis = up
108 | # this I think will be weird with opengl?
109 | {
110 | Vec4.new(fx, lx, ux, px),
111 | Vec4.new(fy, ly, uy, py),
112 | Vec4.new(fz, lz, uz, pz),
113 | Vec4.new(0, 0, 0, 1)
114 | }
115 | end
116 |
117 | @spec scale(mat :: t(), scalar :: number()) :: t()
118 | def scale({r0, r1, r2, r3}, scalar) do
119 | {
120 | Vec4.scale(r0, scalar),
121 | Vec4.scale(r1, scalar),
122 | Vec4.scale(r2, scalar),
123 | Vec4.scale(r3, scalar)
124 | }
125 | end
126 |
127 | @spec add(m1 :: t(), m2 :: t()) :: t()
128 | def add({r0, r1, r2, r3}, {s0, s1, s2, s3}) do
129 | {
130 | Vec4.add(r0, s0),
131 | Vec4.add(r1, s1),
132 | Vec4.add(r2, s2),
133 | Vec4.add(r3, s3)
134 | }
135 | end
136 |
137 | @spec look_at(eye :: Vec3.t(), center :: Vec3.t(), up :: Vec3.t()) :: t()
138 | def look_at(eye, center, up) do
139 | f =
140 | center
141 | |> Vec3.subtract(eye)
142 | |> Vec3.normalize()
143 |
144 | s =
145 | f
146 | |> Vec3.cross(up)
147 | |> Vec3.normalize()
148 |
149 | u = Vec3.cross(s, f)
150 |
151 | {f0, f1, f2} = f
152 | {s0, s1, s2} = s
153 | {u0, u1, u2} = u
154 |
155 | # same not as with orient/2; this coordinate system is wrong for opengl
156 | # .. I think
157 | {
158 | {s0, u0, -f0, 0.0},
159 | {s1, u1, -f1, 0.0},
160 | {s2, u2, -f2, 0.0},
161 | {-Vec3.dot(s, eye), -Vec3.dot(u, eye), Vec3.dot(f, eye), 1.0}
162 | }
163 | |> transpose
164 | end
165 |
166 | @spec perspective(fovy :: float(), aspect :: float(), near :: float(), far :: float()) :: t()
167 | def perspective(fovy, aspect, near, far) do
168 | fovy_radians = fovy * (:math.pi() / 180.0)
169 | f = 1.0 / :math.tan(fovy_radians * 0.5)
170 | y_scale = f / aspect
171 |
172 | {
173 | Vec4.new(f, 0, 0, 0),
174 | Vec4.new(0, y_scale, 0, 0),
175 | Vec4.new(0, 0, (far + near) / (near - far), 2 * far * near / (near - far)),
176 | Vec4.new(0, 0, -1, 0)
177 | }
178 | |> transpose
179 |
180 | # f = 1.0 / :math.tan(fovy / 2)
181 | # nf = 1 / (near - far)
182 |
183 | # {
184 | # {f / aspect, 0.0, 0.0, 0.0},
185 | # {0.0, f, 0.0, 0.0},
186 | # {0.0, 0.0, (far + near) * nf, -1.0},
187 | # {0.0, 0.0, 2 * far * near * nf, 0.0}
188 | # }
189 |
190 | # {
191 | # {f, 0.0, 0.0, 0.0},
192 | # {0.0, f, 0.0, 0.0},
193 | # {0.0, 0.0, (far + near) * nf, -1.0},
194 | # {0.0, 0.0, 2 * far * near * nf, 0.0}
195 | # }
196 | end
197 |
198 | @spec ortho(
199 | x_min :: float(),
200 | x_max :: float(),
201 | y_min :: float(),
202 | y_max :: float(),
203 | z_near :: float(),
204 | z_far :: float()
205 | ) :: t()
206 | def ortho(x_min, x_max, y_min, y_max, z_near, z_far) do
207 | width = x_max - x_min
208 | height = y_max - y_min
209 | depth = z_far - z_near
210 |
211 | tx = -(x_max + x_min) / width
212 | ty = -(y_max + y_min) / height
213 | tz = -(z_far + z_near) / depth
214 |
215 | {
216 | Vec4.new(2 / width, 0, 0, tx),
217 | Vec4.new(0, 2 / height, 0, ty),
218 | Vec4.new(0, 0, -2 / depth, tz),
219 | Vec4.new(0, 0, 0, 1)
220 | }
221 | |> transpose
222 | end
223 |
224 | @spec to_binary(matrix :: t()) :: binary()
225 | def to_binary({a, b, c, d}) do
226 | Vec4.to_binary(a) <> Vec4.to_binary(b) <> Vec4.to_binary(c) <> Vec4.to_binary(d)
227 | end
228 |
229 | @spec multiply_vec(matrix :: t(), vec :: Vec4.t()) :: Vec4.t()
230 | def multiply_vec({a, b, c, d}, vec) do
231 | Vec4.new(
232 | Vec4.dot(a, vec),
233 | Vec4.dot(b, vec),
234 | Vec4.dot(c, vec),
235 | Vec4.dot(d, vec)
236 | )
237 | end
238 |
239 | @spec multiply_mat(mat1 :: t(), mat2 :: t()) :: t()
240 | # and is_float(b00) and is_float(b01) and is_float(b02) and is_float(b03)
241 | # and is_float(b10) and is_float(b11) and is_float(b12) and is_float(b13)
242 | # and is_float(b20) and is_float(b21) and is_float(b22) and is_float(b23)
243 | # and is_float(b30) and is_float(b31) and is_float(b32) and is_float(b33)
244 | def multiply_mat(
245 | {{a00, a01, a02, a03}, {a10, a11, a12, a13}, {a20, a21, a22, a23}, {a30, a31, a32, a33}},
246 | {{b00, b01, b02, b03}, {b10, b11, b12, b13}, {b20, b21, b22, b23}, {b30, b31, b32, b33}}
247 | )
248 | when is_float(a00) and is_float(a01) and is_float(a02) and is_float(a03) and
249 | is_float(a10) and is_float(a11) and is_float(a12) and is_float(a13) and
250 | is_float(a20) and is_float(a21) and is_float(a22) and is_float(a23) and
251 | is_float(a30) and is_float(a31) and is_float(a32) and is_float(a33) do
252 | # Task.async(fn ->
253 | {
254 | {
255 | a00 * b00 + a01 * b10 + a02 * b20 + a03 * b30,
256 | a00 * b01 + a01 * b11 + a02 * b21 + a03 * b31,
257 | a00 * b02 + a01 * b12 + a02 * b22 + a03 * b32,
258 | a00 * b03 + a01 * b13 + a02 * b23 + a03 * b33
259 | },
260 | {
261 | a10 * b00 + a11 * b10 + a12 * b20 + a13 * b30,
262 | a10 * b01 + a11 * b11 + a12 * b21 + a13 * b31,
263 | a10 * b02 + a11 * b12 + a12 * b22 + a13 * b32,
264 | a10 * b03 + a11 * b13 + a12 * b23 + a13 * b33
265 | },
266 | {
267 | a20 * b00 + a21 * b10 + a22 * b20 + a23 * b30,
268 | a20 * b01 + a21 * b11 + a22 * b21 + a23 * b31,
269 | a20 * b02 + a21 * b12 + a22 * b22 + a23 * b32,
270 | a20 * b03 + a21 * b13 + a22 * b23 + a23 * b33
271 | },
272 | {
273 | a30 * b00 + a31 * b10 + a32 * b20 + a33 * b30,
274 | a30 * b01 + a31 * b11 + a32 * b21 + a33 * b31,
275 | a30 * b02 + a31 * b12 + a32 * b22 + a33 * b32,
276 | a30 * b03 + a31 * b13 + a32 * b23 + a33 * b33
277 | }
278 | }
279 |
280 | # end)
281 | # |> Task.await()
282 |
283 | # {
284 | # [
285 | # Task.async(fn ->
286 | # {
287 | # a00 * b00 + a01 * b10 + a02 * b20 + a03 * b30,
288 | # a00 * b01 + a01 * b11 + a02 * b21 + a03 * b31,
289 | # a00 * b02 + a01 * b12 + a02 * b22 + a03 * b32,
290 | # a00 * b03 + a01 * b13 + a02 * b23 + a03 * b33
291 | # }
292 | # end), Task.async(fn ->
293 | # {
294 | # a10 * b00 + a11 * b10 + a12 * b20 + a13 * b30,
295 | # a10 * b01 + a11 * b11 + a12 * b21 + a13 * b31,
296 | # a10 * b02 + a11 * b12 + a12 * b22 + a13 * b32,
297 | # a10 * b03 + a11 * b13 + a12 * b23 + a13 * b33
298 | # }
299 | # end), Task.async(fn -> {
300 | # a20 * b00 + a21 * b10 + a22 * b20 + a23 * b30,
301 | # a20 * b01 + a21 * b11 + a22 * b21 + a23 * b31,
302 | # a20 * b02 + a21 * b12 + a22 * b22 + a23 * b32,
303 | # a20 * b03 + a21 * b13 + a22 * b23 + a23 * b33
304 | # } end), Task.async(fn -> {
305 | # a30 * b00 + a31 * b10 + a32 * b20 + a33 * b30,
306 | # a30 * b01 + a31 * b11 + a32 * b21 + a33 * b31,
307 | # a30 * b02 + a31 * b12 + a32 * b22 + a33 * b32,
308 | # a30 * b03 + a31 * b13 + a32 * b23 + a33 * b33
309 | # } end)
310 | # ]
311 | # |> Task.await_many()
312 | # |> List.to_tuple()
313 | # }
314 |
315 | # |> transpose
316 | end
317 |
318 | @spec translate(mat :: t(), vec :: Vec3.t()) :: t()
319 | def translate(mat, {x, y, z}) do
320 | # {
321 | # {a1, a2, a3, _a4},
322 | # {b1, b2, b3, _b4},
323 | # {c1, c2, c3, _c4},
324 | # d
325 | # } = mat
326 |
327 | transform = {
328 | Vec4.new(1, 0, 0, x),
329 | Vec4.new(0, 1, 0, y),
330 | Vec4.new(0, 0, 1, z),
331 | Vec4.new(0, 0, 0, 1)
332 | }
333 |
334 | multiply_mat(mat, transform)
335 | # |> transpose
336 | end
337 |
338 | def scale_vec(mat, {x, y, z}) do
339 | # {
340 | # {a1, a2, a3, a4},
341 | # {b1, b2, b3, b4},
342 | # {c1, c2, c3, c4},
343 | # d
344 | # } = mat
345 |
346 | transform = {
347 | Vec4.new(x, 0, 0, 0),
348 | Vec4.new(0, y, 0, 0),
349 | Vec4.new(0, 0, z, 0),
350 | Vec4.new(0, 0, 0, 1)
351 | }
352 |
353 | multiply_mat(mat, transform)
354 | # |> transpose
355 | end
356 |
357 | def rotate(mat, angle, {x, y, z}) do
358 | c = :math.cos(angle)
359 | s = :math.sin(angle)
360 |
361 | transform = {
362 | Vec4.new(c + x * x * (1 - c), x * y * (1 - c) - z * s, x * z * (1 - c) + y * s, 0),
363 | Vec4.new(y * x * (1 - c) + z * s, c + y * y * (1 - c), y * z * (1 - c) - x * s, 0),
364 | Vec4.new(z * x * (1 - c) - y * s, z * y * (1 - c) + x * s, c + z * z * (1 - c), 0),
365 | Vec4.new(0, 0, 0, 1)
366 | }
367 |
368 | multiply_mat(mat, transform)
369 | end
370 | end
371 |
--------------------------------------------------------------------------------
/lib/breakout/math/quat.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Math.Quat do
2 | alias Breakout.Math.Vec4
3 | alias Breakout.Math.Vec3
4 | alias Breakout.Math.Mat3
5 |
6 | @type t :: {float(), float(), float(), float()}
7 |
8 | @spec new(n :: Vec3.t(), angle_rad :: number()) :: t()
9 | def new(n, angle_rad) do
10 | half_angle_rad = 0.5 * angle_rad
11 |
12 | w = :math.cos(half_angle_rad)
13 | half_sine = :math.sin(half_angle_rad)
14 | {x, y, z} = Vec3.normalize(n)
15 |
16 | {x * half_sine, y * half_sine, z * half_sine, w}
17 | end
18 |
19 | @spec multiply(lhs :: t(), rhs :: t()) :: t()
20 | def multiply({x1, y1, z1, w1}, {x2, y2, z2, w2}) do
21 | {
22 | x1 * w2 + w1 * x2 + y1 * z2 - z1 * y2,
23 | y1 * w2 + w1 * y2 + z1 * x2 - x1 * z2,
24 | z1 * w2 + w1 * z2 + x1 * y2 - y1 * x2,
25 | w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
26 | }
27 | end
28 |
29 | @spec scale(quat :: t(), scalar :: float()) :: t()
30 | def scale({x, y, z, w}, scalar) do
31 | {
32 | x * scalar,
33 | y * scalar,
34 | z * scalar,
35 | w * scalar
36 | }
37 | end
38 |
39 | @spec normalize(quat :: t()) :: t()
40 | def normalize({x, y, z, w} = q) do
41 | inv_mag = 1 / magnitude(q)
42 |
43 | {
44 | x * inv_mag,
45 | y * inv_mag,
46 | z * inv_mag,
47 | w * inv_mag
48 | }
49 | end
50 |
51 | @spec invert(quat :: t()) :: t()
52 | def invert(q) do
53 | {x, y, z, w} = scale(q, 1 / magnitude_squared(q))
54 |
55 | {-x, -y, -z, w}
56 | end
57 |
58 | # this is just invert, but the C++ source does a copy. unnecessary here.
59 | # curious to see which is used more.
60 | @spec inverse(quat :: t()) :: t()
61 | def inverse(q) do
62 | invert(q)
63 | end
64 |
65 | @spec magnitude(quat :: t()) :: float()
66 | def magnitude(q) do
67 | :math.sqrt(magnitude_squared(q))
68 | end
69 |
70 | @spec magnitude_squared(quat :: t()) :: float()
71 | def magnitude_squared({x, y, z, w}) do
72 | x * x + y * y + z * z + w * w
73 | end
74 |
75 | @spec rotate_point(quat :: t(), vec :: Vec3.t()) :: Vec3.t()
76 | def rotate_point(q, {x, y, z}) do
77 | vector = {x, y, z, 0.0}
78 |
79 | {x, y, z, _} =
80 | q
81 | |> multiply(vector)
82 | |> multiply(inverse(q))
83 |
84 | {x, y, z}
85 | end
86 |
87 | @spec rotate_matrix(quat :: t(), mat :: Mat3.t()) :: Mat3.t()
88 | def rotate_matrix(q, {r0, r1, r2}) do
89 | {
90 | rotate_point(q, r0),
91 | rotate_point(q, r1),
92 | rotate_point(q, r2)
93 | }
94 | end
95 |
96 | @spec to_mat3(quat :: t) :: Mat3.t()
97 | def to_mat3(q) do
98 | {r0, r1, r2} = Mat3.identity()
99 |
100 | {
101 | rotate_point(q, r0),
102 | rotate_point(q, r1),
103 | rotate_point(q, r2)
104 | }
105 | end
106 |
107 | def to_mat4(q) do
108 | {{a1, a2, a3}, {b1, b2, b3}, {c1, c2, c3}} = to_mat3(q)
109 |
110 | {
111 | Vec4.new(a1, a2, a3, 0),
112 | Vec4.new(b1, b2, b3, 0),
113 | Vec4.new(c1, c2, c3, 0),
114 | Vec4.new(0, 0, 0, 1)
115 | }
116 | end
117 | end
118 |
--------------------------------------------------------------------------------
/lib/breakout/math/vec2.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Math.Vec2 do
2 | @type t :: {float(), float()}
3 |
4 | @spec new(x :: number(), y :: number()) :: t()
5 | def new(x, y) do
6 | {x + 0.0, y + 0.0}
7 | end
8 |
9 | @spec add(v1 :: t(), v2 :: t()) :: t()
10 | def add({x1, y1}, {x2, y2}) do
11 | {x1 + x2, y1 + y2}
12 | end
13 |
14 | @spec subtract(v1 :: t(), v2 :: t()) :: t()
15 | def subtract({x1, y1}, {x2, y2}) do
16 | {x1 - x2, y1 - y2}
17 | end
18 |
19 | @spec scale(v :: t(), scalar :: number()) :: t()
20 | def scale({x, y}, scalar) do
21 | {x * scalar, y * scalar}
22 | end
23 |
24 | @spec multiply(v1 :: t(), v2 :: t()) :: t()
25 | def multiply({x1, y1}, {x2, y2}) do
26 | {x1 * x2, y1 * y2}
27 | end
28 |
29 | @spec normalize(v :: t()) :: t()
30 | def normalize({x, y} = v) do
31 | mag = magnitude(v)
32 |
33 | inv_mag =
34 | try do
35 | 1 / mag
36 | rescue
37 | _ -> 0
38 | end
39 |
40 | {x * inv_mag, y * inv_mag}
41 | end
42 |
43 | @spec magnitude(v :: t()) :: float()
44 | def magnitude({x, y}) do
45 | :math.sqrt(x * x + y * y)
46 | end
47 |
48 | @spec length(v :: t()) :: float()
49 | def length(v) do
50 | magnitude(v)
51 | end
52 |
53 | @spec dot(v1 :: t(), v2 :: t()) :: float()
54 | def dot({x1, y1}, {x2, y2}) do
55 | x1 * x2 + y1 * y2
56 | end
57 |
58 | @spec clamp(value :: t(), min_val :: t(), max_val :: t()) :: t()
59 | def clamp({x, y}, {min_x, min_y}, {max_x, max_y}) do
60 | {max(min_x, min(max_x, x)), max(min_y, min(max_y, y))}
61 | end
62 |
63 | @compass [
64 | {0.0, 1.0},
65 | {1.0, 0.0},
66 | {0.0, -1.0},
67 | {-1.0, 0.0}
68 | ]
69 | @spec direction(target :: t()) :: :up | :right | :down | :left
70 | def direction(target) do
71 | target = normalize(target)
72 |
73 | {best_match, _} =
74 | @compass
75 | |> Enum.with_index()
76 | |> Enum.map(fn {direction, index} -> {index, dot(target, direction)} end)
77 | |> Enum.max_by(fn {_, dot_product} -> dot_product end)
78 |
79 | direction_from_index(best_match)
80 | end
81 |
82 | defp direction_from_index(0), do: :up
83 | defp direction_from_index(1), do: :right
84 | defp direction_from_index(2), do: :down
85 | defp direction_from_index(3), do: :left
86 | end
87 |
--------------------------------------------------------------------------------
/lib/breakout/math/vec3.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Math.Vec3 do
2 | @type t :: {float(), float(), float()}
3 |
4 | @spec new(x :: number(), y :: number(), z :: number()) :: t()
5 | def new(x, y, z) do
6 | {x + 0.0, y + 0.0, z + 0.0}
7 | end
8 |
9 | @spec add(vec1 :: t(), vec2 :: t()) :: t()
10 | def add({x1, y1, z1}, {x2, y2, z2}) do
11 | {x1 + x2, y1 + y2, z1 + z2}
12 | end
13 |
14 | @spec cross(vec1 :: t(), vec2 :: t()) :: t()
15 | def cross({x1, y1, z1}, {x2, y2, z2}) do
16 | {
17 | y1 * z2 - z1 * y2,
18 | z1 * x2 - x1 * z2,
19 | x1 * y2 - y1 * x2
20 | }
21 | end
22 |
23 | @spec dot(vec1 :: t(), vec2 :: t()) :: float()
24 | def dot({x1, y1, z1}, {x2, y2, z2}) do
25 | x1 * x2 + y1 * y2 + z1 * z2
26 | end
27 |
28 | @spec normalize(vec :: t()) :: t()
29 | def normalize({x, y, z} = vec) do
30 | mag = magnitude(vec)
31 | inv_mag = if mag == 0.0, do: 0, else: 1 / mag
32 | {x * inv_mag, y * inv_mag, z * inv_mag}
33 | end
34 |
35 | @spec magnitude(vec :: t()) :: float()
36 | def magnitude({x, y, z}) do
37 | :math.sqrt(x * x + y * y + z * z)
38 | end
39 |
40 | @spec scale(vec :: t(), scalar :: float()) :: t()
41 | def scale({x, y, z}, scalar) do
42 | {x * scalar, y * scalar, z * scalar}
43 | end
44 |
45 | @spec get_ortho(this :: t()) :: {t(), t()}
46 | def get_ortho(n) do
47 | n = normalize(n)
48 | {_, _, nz} = n
49 | w = if nz * nz > 0.9 * 0.9, do: new(1, 0, 0), else: new(0, 0, 1)
50 | u = normalize(cross(w, n))
51 | v = normalize(cross(n, u))
52 | u = normalize(cross(v, n))
53 |
54 | {u, v}
55 | end
56 |
57 | @spec subtract(vec1 :: t(), vec2 :: t()) :: t()
58 | def subtract({x1, y1, z1}, {x2, y2, z2}) do
59 | {x1 - x2, y1 - y2, z1 - z2}
60 | end
61 |
62 | @spec to_binary(vec :: t()) :: binary()
63 | def to_binary({x, y, z}) do
64 | <>
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/lib/breakout/math/vec4.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Math.Vec4 do
2 | @type t :: {float(), float(), float(), float()}
3 |
4 | @spec new(x :: number(), y :: number(), z :: number(), w :: number()) :: t()
5 | def new(x, y, z, w) do
6 | {x + 0.0, y + 0.0, z + 0.0, w + 0.0}
7 | end
8 |
9 | @spec add(vec1 :: t(), vec2 :: t()) :: t()
10 | def add({x1, y1, z1, w1}, {x2, y2, z2, w2}) do
11 | {x1 + x2, y1 + y2, z1 + z2, w1 + w2}
12 | end
13 |
14 | @spec normalize(vec :: t()) :: t()
15 | def normalize({x, y, z, w} = vec) do
16 | inv_mag = 1 / magnitude(vec)
17 | {x * inv_mag, y * inv_mag, z * inv_mag, w * inv_mag}
18 | end
19 |
20 | @spec magnitude(vec :: t()) :: float()
21 | def magnitude({x, y, z, w}) do
22 | :math.sqrt(x * x + y * y + z * z + w * w)
23 | end
24 |
25 | @spec scale(vec :: t(), scalar :: float()) :: t()
26 | def scale({x, y, z, w}, scalar) do
27 | {x * scalar, y * scalar, z * scalar, w * scalar}
28 | end
29 |
30 | @spec subtract(vec1 :: t(), vec2 :: t()) :: t()
31 | def subtract({x1, y1, z1, w1}, {x2, y2, z2, w2}) do
32 | {x1 - x2, y1 - y2, z1 - z2, w1 - w2}
33 | end
34 |
35 | @spec dot(vec1 :: t(), vec2 :: t()) :: float()
36 | def dot({x1, y1, z1, w1}, {x2, y2, z2, w2}) do
37 | x1 * x2 + y1 * y2 + z1 * z2 + w1 * w2
38 | end
39 |
40 | @spec to_binary(vec :: t()) :: binary()
41 | def to_binary({x, y, z, w}) do
42 | <>
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/breakout/particle.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Particle do
2 | alias Breakout.Math.Vec2
3 | alias Breakout.Math.Vec4
4 |
5 | defstruct position: Vec2.new(0, 0),
6 | velocity: Vec2.new(0, 0),
7 | color: Vec4.new(1, 1, 1, 1),
8 | life: 0.0
9 |
10 | @type t :: %__MODULE__{
11 | position: Vec2.t(),
12 | velocity: Vec2.t(),
13 | color: Vec4.t(),
14 | life: float()
15 | }
16 | end
17 |
--------------------------------------------------------------------------------
/lib/breakout/particle_generator.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.ParticleGenerator do
2 | alias Breakout.Math.Vec4
3 | alias Breakout.Math.Vec2
4 | alias Breakout.GameObject
5 | alias Breakout.Renderer.Texture2D
6 | alias Breakout.Renderer.Shader
7 | alias Breakout.Particle
8 | alias Breakout.GameObject
9 | alias Breakout.Util
10 |
11 | defstruct particles: [],
12 | amount: 0,
13 | shader: nil,
14 | texture: nil,
15 | vao: 0
16 |
17 | @type t :: %__MODULE__{
18 | particles: [Particle.t()],
19 | amount: non_neg_integer(),
20 | shader: Shader.t(),
21 | texture: Texture2D.t(),
22 | vao: non_neg_integer()
23 | }
24 |
25 | @spec new(shader :: Shader.t(), texture :: Texture2D.t(), amount :: non_neg_integer()) :: t()
26 | def new(shader, texture, amount) do
27 | %__MODULE__{
28 | particles: [],
29 | amount: amount,
30 | shader: shader,
31 | texture: texture
32 | }
33 | |> init()
34 | end
35 |
36 | @spec update(
37 | pg :: t(),
38 | dt :: float(),
39 | object :: GameObject.t(),
40 | new_particles :: non_neg_integer(),
41 | offset :: Vec2.t()
42 | ) :: t()
43 | def update(pg, dt, object, new_particles, offset) do
44 | generator =
45 | Enum.reduce(1..new_particles, pg, fn _, acc ->
46 | unused_particle = first_unused_particle(acc)
47 | respawn_particle(acc, unused_particle, object, offset)
48 | end)
49 |
50 | updated_particles =
51 | Enum.map(generator.particles, fn particle ->
52 | update_particle(particle, dt)
53 | end)
54 |
55 | %{generator | particles: updated_particles}
56 | end
57 |
58 | def update_particle(%Particle{} = particle, dt) do
59 | life = particle.life - 0.001 * dt
60 |
61 | if life > 0.0 do
62 | position = Vec2.subtract(particle.position, Vec2.scale(particle.velocity, 0.1))
63 | color = put_elem(particle.color, 3, (particle.color |> elem(3)) - 0.01)
64 | %Particle{particle | position: position, color: color, life: life}
65 | else
66 | %Particle{particle | life: life}
67 | end
68 | end
69 |
70 | def draw(%__MODULE__{} = pg) do
71 | :gl.blendFunc(:gl_const.gl_src_alpha(), :gl_const.gl_one())
72 | Shader.use_shader(pg.shader)
73 |
74 | pg.particles
75 | |> Enum.each(fn particle ->
76 | if particle.life > 0.0 do
77 | pg.shader
78 | |> Shader.set(~c"offset", particle.position)
79 | |> Shader.set(~c"color", particle.color)
80 |
81 | Texture2D.bind(pg.texture)
82 |
83 | :gl.bindVertexArray(pg.vao)
84 | :gl.drawArrays(:gl_const.gl_triangles(), 0, 6)
85 | :gl.bindVertexArray(0)
86 | end
87 | end)
88 |
89 | :gl.blendFunc(:gl_const.gl_src_alpha(), :gl_const.gl_one_minus_src_alpha())
90 | end
91 |
92 | defp init(pg) do
93 | particle_quad =
94 | Util.make_bits([
95 | 0.0,
96 | 1.0,
97 | 0.0,
98 | 1.0,
99 | 1.0,
100 | 0.0,
101 | 1.0,
102 | 0.0,
103 | 0.0,
104 | 0.0,
105 | 0.0,
106 | 0.0,
107 | 0.0,
108 | 1.0,
109 | 0.0,
110 | 1.0,
111 | 1.0,
112 | 1.0,
113 | 1.0,
114 | 1.0,
115 | 1.0,
116 | 0.0,
117 | 1.0,
118 | 0.0
119 | ])
120 |
121 | [vao] = :gl.genVertexArrays(1)
122 | [vbo] = :gl.genBuffers(1)
123 | :gl.bindVertexArray(vao)
124 |
125 | :gl.bindBuffer(:gl_const.gl_array_buffer(), vbo)
126 |
127 | :gl.bufferData(
128 | :gl_const.gl_array_buffer(),
129 | byte_size(particle_quad),
130 | particle_quad,
131 | :gl_const.gl_static_draw()
132 | )
133 |
134 | :gl.enableVertexAttribArray(0)
135 |
136 | :gl.vertexAttribPointer(
137 | 0,
138 | 4,
139 | :gl_const.gl_float(),
140 | :gl_const.gl_false(),
141 | 4 * byte_size(<<0.0::float-native-size(32)>>),
142 | 0
143 | )
144 |
145 | :gl.bindVertexArray(0)
146 |
147 | particles =
148 | for _ <- 0..pg.amount do
149 | %Particle{}
150 | end
151 |
152 | %__MODULE__{pg | vao: vao, particles: particles}
153 | end
154 |
155 | defp first_unused_particle(%__MODULE__{particles: particles}) do
156 | Enum.find_index(particles, fn p -> p.life <= 0 end) || 0
157 | end
158 |
159 | defp respawn_particle(%__MODULE__{} = pg, index, %GameObject{} = object, offset) do
160 | random = (:rand.uniform(100) - 50) / 10.0
161 | r_color = 0.5 + :rand.uniform() / 2.0
162 | position = Vec2.add(object.position, Vec2.add(offset, {random, random}))
163 | color = Vec4.new(r_color, r_color, r_color, 1)
164 | velocity = Vec2.scale(object.velocity, 0.1)
165 |
166 | particle = %Particle{position: position, color: color, life: 1.0, velocity: velocity}
167 | updated_particles = List.update_at(pg.particles, index, fn _ -> particle end)
168 |
169 | %__MODULE__{
170 | pg
171 | | particles: updated_particles
172 | }
173 | end
174 | end
175 |
--------------------------------------------------------------------------------
/lib/breakout/player.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Player do
2 | alias Breakout.Math.Vec2
3 |
4 | @type t :: %__MODULE__{
5 | size: Vec2.t(),
6 | velocity: number(),
7 | position: Vec2.t()
8 | }
9 |
10 | defstruct size: Vec2.new(100, 20),
11 | velocity: 500.0,
12 | position: Vec2.new(0, 0)
13 | end
14 |
--------------------------------------------------------------------------------
/lib/breakout/power_up.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.PowerUp do
2 | alias Breakout.Renderer.Texture2D
3 | alias Breakout.Math.Vec2
4 | alias Breakout.Math.Vec3
5 | alias Breakout.GameObject
6 |
7 | @type power_up_types ::
8 | nil | :chaos | :confuse | :increase | :passthrough | :speed | :sticky
9 |
10 | @type t :: %__MODULE__{
11 | type: power_up_types(),
12 | duration: float(),
13 | activated: boolean(),
14 | game_object: GameObject.t()
15 | }
16 |
17 | @enforce_keys [:game_object]
18 | defstruct type: nil,
19 | duration: 0.0,
20 | activated: false,
21 | game_object: GameObject.new()
22 |
23 | @spec new(
24 | type :: power_up_types(),
25 | color :: Vec3.t(),
26 | duration :: float(),
27 | position :: Vec2.t(),
28 | texture :: Texture2D.t()
29 | ) :: t()
30 | def new(type, color, duration, position, texture) do
31 | %__MODULE__{
32 | type: type,
33 | duration: duration,
34 | game_object:
35 | GameObject.new(
36 | position,
37 | Vec2.new(60, 20),
38 | texture,
39 | color,
40 | Vec2.new(0, 150)
41 | )
42 | }
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/breakout/renderer/open_gl.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Renderer.OpenGL do
2 | def init() do
3 | do_enables()
4 |
5 | :gl.blendFunc(:gl_const.gl_src_alpha(), :gl_const.gl_one_minus_src_alpha())
6 | end
7 |
8 | defp do_enables() do
9 | # :gl.enable(:gl_const.gl_depth_test)
10 | # :gl.enable(:gl_const.gl_cull_face)
11 | :gl.enable(:gl_const.gl_multisample())
12 | :gl.enable(:gl_const.gl_blend())
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/breakout/renderer/post_processor.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Renderer.PostProcessor do
2 | require Logger
3 | alias Breakout.Util
4 | alias Breakout.Renderer.{Shader, Texture2D}
5 |
6 | @type t :: %__MODULE__{
7 | shader: Shader.t(),
8 | texture: Texture2D.t(),
9 | width: non_neg_integer(),
10 | height: non_neg_integer(),
11 | confuse: boolean(),
12 | chaos: boolean(),
13 | shake: boolean(),
14 | msfbo: non_neg_integer(),
15 | fbo: non_neg_integer(),
16 | rbo: non_neg_integer(),
17 | vao: non_neg_integer()
18 | }
19 |
20 | defstruct shader: nil,
21 | texture: nil,
22 | width: 0,
23 | height: 0,
24 | confuse: false,
25 | chaos: false,
26 | shake: false,
27 | msfbo: 0,
28 | fbo: 0,
29 | rbo: 0,
30 | vao: 0
31 |
32 | @spec new(shader :: Shader.t(), width :: non_neg_integer(), height :: non_neg_integer()) :: t()
33 | def new(shader, width, height) do
34 | pp = %__MODULE__{
35 | shader: shader,
36 | width: width,
37 | height: height
38 | }
39 |
40 | [msfbo, fbo] = :gl.genFramebuffers(2)
41 | :gl.isFramebuffer(msfbo)
42 | :gl.isFramebuffer(fbo)
43 | [rbo] = :gl.genRenderbuffers(1)
44 | :gl.isRenderbuffer(rbo)
45 |
46 | :gl.bindFramebuffer(:gl_const.gl_framebuffer(), msfbo)
47 |
48 | :gl.flush()
49 |
50 | :gl.bindRenderbuffer(:gl_const.gl_renderbuffer(), rbo)
51 |
52 | :gl.renderbufferStorageMultisample(
53 | :gl_const.gl_renderbuffer(),
54 | 4,
55 | :gl_const.gl_rgb(),
56 | width,
57 | height
58 | )
59 |
60 | :gl.framebufferRenderbuffer(
61 | :gl_const.gl_framebuffer(),
62 | :gl_const.gl_color_attachment0(),
63 | :gl_const.gl_renderbuffer(),
64 | rbo
65 | )
66 |
67 | if :gl.checkFramebufferStatus(:gl_const.gl_framebuffer()) !=
68 | :gl_const.gl_framebuffer_complete() do
69 | Logger.error("failed to init msfbo")
70 | end
71 |
72 | :gl.bindFramebuffer(:gl_const.gl_framebuffer(), fbo)
73 |
74 | tex =
75 | Texture2D.new()
76 | |> Texture2D.generate(width, height, 0)
77 |
78 | :gl.framebufferTexture2D(
79 | :gl_const.gl_framebuffer(),
80 | :gl_const.gl_color_attachment0(),
81 | :gl_const.gl_texture_2d(),
82 | tex.id,
83 | 0
84 | )
85 |
86 | pp = %__MODULE__{pp | texture: tex, msfbo: msfbo, fbo: fbo, rbo: rbo}
87 |
88 | if :gl.checkFramebufferStatus(:gl_const.gl_framebuffer()) !=
89 | :gl_const.gl_framebuffer_complete() do
90 | Logger.error("failed to init fbo")
91 | end
92 |
93 | :gl.bindFramebuffer(:gl_const.gl_framebuffer(), 0)
94 |
95 | pp = init_render_data(pp)
96 |
97 | Shader.set(pp.shader, ~c"scene", 0, true)
98 | offset = 1.0 / 300.0
99 |
100 | offsets = [
101 | {-offset, offset},
102 | {0.0, offset},
103 | {offset, offset},
104 | {-offset, 0.0},
105 | {0.0, 0.0},
106 | {offset, 0.0},
107 | {-offset, -offset},
108 | {0.0, -offset},
109 | {offset, -offset}
110 | ]
111 |
112 | Shader.set(pp.shader, ~c"offsets", offsets)
113 |
114 | edge_kernel = [
115 | -1,
116 | -1,
117 | -1,
118 | -1,
119 | 8,
120 | -1,
121 | -1,
122 | -1,
123 | -1
124 | ]
125 |
126 | Shader.set(pp.shader, ~c"edge_kernel", edge_kernel)
127 |
128 | blur_kernel = [
129 | 1 / 16,
130 | 2 / 16,
131 | 1 / 16,
132 | 2 / 16,
133 | 4 / 16,
134 | 2 / 16,
135 | 1 / 16,
136 | 2 / 16,
137 | 1 / 16
138 | ]
139 |
140 | Shader.set(pp.shader, ~c"blur_kernel", blur_kernel)
141 |
142 | pp
143 | end
144 |
145 | @spec begin_render(post_processor :: t()) :: :ok
146 | def begin_render(%__MODULE__{msfbo: msfbo}) do
147 | :gl.bindFramebuffer(:gl_const.gl_framebuffer(), msfbo)
148 | :gl.clearColor(0.0, 0.0, 0.0, 1.0)
149 | :gl.clear(:gl_const.gl_color_buffer_bit())
150 | end
151 |
152 | @spec end_render(post_processor :: t()) :: :ok
153 | def end_render(%__MODULE__{msfbo: msfbo, fbo: fbo, width: width, height: height}) do
154 | :gl.bindFramebuffer(:gl_const.gl_read_framebuffer(), msfbo)
155 | :gl.bindFramebuffer(:gl_const.gl_draw_framebuffer(), fbo)
156 |
157 | :gl.blitFramebuffer(
158 | 0,
159 | 0,
160 | width,
161 | height,
162 | 0,
163 | 0,
164 | width,
165 | height,
166 | :gl_const.gl_color_buffer_bit(),
167 | :gl_const.gl_nearest()
168 | )
169 |
170 | :gl.bindFramebuffer(:gl_const.gl_framebuffer(), 0)
171 | end
172 |
173 | @spec render(post_processor :: t(), time :: float()) :: :ok
174 | def render(
175 | %__MODULE__{
176 | shader: shader,
177 | confuse: confuse,
178 | chaos: chaos,
179 | shake: shake,
180 | texture: texture,
181 | vao: vao
182 | },
183 | time
184 | )
185 | when is_float(time) do
186 | Shader.use_shader(shader)
187 | |> Shader.set(~c"time", time)
188 | |> Shader.set(~c"confuse", confuse)
189 | |> Shader.set(~c"chaos", chaos)
190 | |> Shader.set(~c"shake", shake)
191 |
192 | :gl.activeTexture(:gl_const.gl_texture0())
193 | Texture2D.bind(texture)
194 |
195 | :gl.bindVertexArray(vao)
196 | :gl.drawArrays(:gl_const.gl_triangles(), 0, 6)
197 | :gl.bindVertexArray(0)
198 | end
199 |
200 | @spec init_render_data(post_processor :: t()) :: t()
201 | def init_render_data(%__MODULE__{} = post_processor) do
202 | vertices =
203 | Util.make_bits([
204 | -1,
205 | -1,
206 | 0,
207 | 0,
208 | 1,
209 | 1,
210 | 1,
211 | 1,
212 | -1,
213 | 1,
214 | 0,
215 | 1,
216 | -1,
217 | -1,
218 | 0,
219 | 0,
220 | 1,
221 | -1,
222 | 1,
223 | 0,
224 | 1,
225 | 1,
226 | 1,
227 | 1
228 | ])
229 |
230 | [vao] = :gl.genVertexArrays(1)
231 | [vbo] = :gl.genBuffers(1)
232 |
233 | :gl.bindBuffer(:gl_const.gl_array_buffer(), vbo)
234 |
235 | :gl.bufferData(
236 | :gl_const.gl_array_buffer(),
237 | byte_size(vertices),
238 | vertices,
239 | :gl_const.gl_static_draw()
240 | )
241 |
242 | :gl.bindVertexArray(vao)
243 | :gl.enableVertexAttribArray(0)
244 |
245 | :gl.vertexAttribPointer(
246 | 0,
247 | 4,
248 | :gl_const.gl_float(),
249 | :gl_const.gl_false(),
250 | 4 * byte_size(<<0.0::native-float-size(32)>>),
251 | 0
252 | )
253 |
254 | :gl.bindBuffer(:gl_const.gl_array_buffer(), 0)
255 | :gl.bindVertexArray(0)
256 |
257 | %__MODULE__{post_processor | vao: vao}
258 | end
259 | end
260 |
--------------------------------------------------------------------------------
/lib/breakout/renderer/renderer.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Renderer do
2 | require Logger
3 |
4 | alias Breakout.Math.Vec3
5 | alias Breakout.Math.Vec2
6 | # alias Breakout.ResourceManager
7 | alias Breakout.Renderer.Sprite
8 | @spec draw(state :: Breakout.State.t()) :: :ok
9 | def draw(state) do
10 | case state.resources.textures[:face] do
11 | {:ok, texture} ->
12 | t = :erlang.monotonic_time(:millisecond)
13 |
14 | Sprite.draw(
15 | state.sprite_renderer,
16 | texture,
17 | Vec2.new(:math.cos(t / 1000) * 550 + 550, :math.sin(t / 1000) * 325 + 325),
18 | Vec2.new(100, 100),
19 | :math.sin(t / 1000) * :math.pi(),
20 | Vec3.new(0, 1, 0)
21 | )
22 |
23 | nil ->
24 | nil
25 | end
26 |
27 | :ok
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/breakout/renderer/shader.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Renderer.Shader do
2 | require Logger
3 |
4 | @type t :: integer()
5 | @type shader_type :: non_neg_integer()
6 | @type uniform_type ::
7 | float()
8 | | integer()
9 | # Breakout.Vec2.t() | Breakout.Vec3.t() | Breakout.Vec4.t() | Breakout.Mat4.t()
10 | | {float(), float()}
11 | | {float(), float(), float()}
12 | | {float(), float(), float(), float()}
13 |
14 | @spec init(vertex_path :: Path.t(), fragment_path :: Path.t()) :: t()
15 | def init(vertex_path, fragment_path) do
16 | vertex_code = File.read!(vertex_path)
17 | fragment_code = File.read!(fragment_path)
18 |
19 | vertex_shader = compile_shader(vertex_code, :gl_const.gl_vertex_shader())
20 | fragment_shader = compile_shader(fragment_code, :gl_const.gl_fragment_shader())
21 |
22 | shader_program = :gl.createProgram()
23 | :gl.attachShader(shader_program, vertex_shader)
24 | :gl.attachShader(shader_program, fragment_shader)
25 | :gl.linkProgram(shader_program)
26 | check_program_linking!(shader_program)
27 |
28 | :gl.deleteShader(vertex_shader)
29 | :gl.deleteShader(fragment_shader)
30 |
31 | shader_program
32 | end
33 |
34 | @spec compile_shader(source :: binary(), type :: shader_type()) :: t()
35 | defp compile_shader(source, type) do
36 | shader = :gl.createShader(type)
37 | :gl.shaderSource(shader, [source <> <<0>>])
38 | :gl.compileShader(shader)
39 | check_shader_compilation!(shader)
40 |
41 | shader
42 | end
43 |
44 | @spec check_shader_compilation!(shader :: integer()) :: :ok
45 | defp check_shader_compilation!(shader) do
46 | status = :gl.getShaderiv(shader, :gl_const.gl_compile_status())
47 |
48 | unless status == :gl_const.gl_true() do
49 | buf_size = :gl.getShaderiv(shader, :gl_const.gl_info_log_length())
50 | info_log = :gl.getShaderInfoLog(shader, buf_size)
51 | Logger.error(msg: info_log)
52 | raise "Shader compilation error: #{info_log}"
53 | end
54 |
55 | :ok
56 | end
57 |
58 | @spec check_program_linking!(program :: integer()) :: :ok
59 | defp check_program_linking!(program) do
60 | status = :gl.getProgramiv(program, :gl_const.gl_link_status())
61 |
62 | unless status == :gl_const.gl_true() do
63 | buf_size = :gl.getProgramiv(program, :gl_const.gl_info_log_length())
64 | info_log = :gl.getProgramInfoLog(program, buf_size)
65 | Logger.error(msg: info_log)
66 | raise "Program linking error: #{info_log}"
67 | end
68 |
69 | :ok
70 | end
71 |
72 | @spec use_shader(shader :: t()) :: t()
73 | def use_shader(shader) do
74 | :gl.useProgram(shader)
75 |
76 | shader
77 | end
78 |
79 | # @spec set(shader :: pos_integer(), name :: binary(), value :: uniform_type()) :: :ok
80 | def set(shader, name, value, use_shader \\ false)
81 |
82 | def set(shader, name, value, use_shader) when is_float(value) do
83 | if use_shader, do: use_shader(shader)
84 |
85 | :gl.uniform1f(:gl.getUniformLocation(shader, name), value)
86 |
87 | shader
88 | end
89 |
90 | def set(shader, name, value, use_shader) when is_integer(value) do
91 | if use_shader, do: use_shader(shader)
92 |
93 | :gl.uniform1i(:gl.getUniformLocation(shader, name), value)
94 |
95 | shader
96 | end
97 |
98 | def set(shader, name, true, use_shader), do: set(shader, name, 1, use_shader)
99 |
100 | def set(shader, name, false, use_shader), do: set(shader, name, 0, use_shader)
101 |
102 | def set(shader, name, {x, y}, use_shader) do
103 | if use_shader, do: use_shader(shader)
104 |
105 | :gl.uniform2f(:gl.getUniformLocation(shader, name), x, y)
106 |
107 | shader
108 | end
109 |
110 | def set(shader, name, {x, y, z}, use_shader) do
111 | if use_shader, do: use_shader(shader)
112 |
113 | :gl.uniform3f(:gl.getUniformLocation(shader, name), x, y, z)
114 |
115 | shader
116 | end
117 |
118 | def set(shader, name, {x, y, z, w}, use_shader) do
119 | if use_shader, do: use_shader(shader)
120 |
121 | :gl.uniform4f(:gl.getUniformLocation(shader, name), x, y, z, w)
122 |
123 | shader
124 | end
125 |
126 | def set(shader, name, value, use_shader)
127 | when is_list(value) and tuple_size(value |> hd()) == 16 do
128 | if use_shader, do: use_shader(shader)
129 |
130 | :gl.uniformMatrix4fv(:gl.getUniformLocation(shader, name), :gl_const.gl_false(), value)
131 |
132 | shader
133 | end
134 |
135 | def set(shader, name, value, use_shader)
136 | when is_list(value) and tuple_size(value |> hd()) == 2 do
137 | if use_shader, do: use_shader(shader)
138 |
139 | :gl.uniform2fv(:gl.getUniformLocation(shader, name), value)
140 | end
141 |
142 | def set(shader, name, value, use_shader) when is_list(value) and is_integer(value |> hd()) do
143 | if use_shader, do: use_shader(shader)
144 |
145 | :gl.uniform1iv(:gl.getUniformLocation(shader, name), value)
146 | end
147 |
148 | def set(shader, name, value, use_shader) when is_list(value) and is_float(value |> hd()) do
149 | if use_shader, do: use_shader(shader)
150 |
151 | :gl.uniform1fv(:gl.getUniformLocation(shader, name), value)
152 | end
153 | end
154 |
--------------------------------------------------------------------------------
/lib/breakout/renderer/sprite.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Renderer.Sprite do
2 | alias Breakout.Renderer.Texture2D
3 | alias Breakout.Math.Vec3
4 | alias Breakout.Math.Mat4
5 | alias Breakout.Renderer.Shader
6 | alias Breakout.Util
7 |
8 | defstruct [:shader, :quadVAO]
9 |
10 | def new(shader) do
11 | %__MODULE__{
12 | shader: shader
13 | }
14 | |> init_render_data()
15 | end
16 |
17 | def draw(
18 | # %__MODULE__{shader: shader} = sprite,
19 | %_{sprite_renderer: %{shader: shader} = sprite, resources: resources} = _state,
20 | texture,
21 | {x, y} = _position,
22 | {width, height} = _size,
23 | rotate,
24 | color
25 | ) do
26 | texture = resources.textures[texture]
27 | Shader.use_shader(shader)
28 |
29 | model =
30 | Mat4.identity()
31 | |> Mat4.translate(Vec3.new(x, y, 0))
32 | |> Mat4.translate(Vec3.new(0.5 * width, 0.5 * height, 0))
33 | |> Mat4.rotate(rotate, Vec3.new(0, 0, 1))
34 | |> Mat4.translate(Vec3.new(-0.5 * width, -0.5 * height, 0))
35 | |> Mat4.scale_vec(Vec3.new(width, height, 1))
36 | |> Mat4.transpose()
37 |
38 | Shader.set(shader, ~c"model", [model |> Mat4.flatten()])
39 | Shader.set(shader, ~c"spriteColor", color)
40 |
41 | :gl.activeTexture(:gl_const.gl_texture0())
42 | Texture2D.bind(texture)
43 |
44 | :gl.bindVertexArray(sprite.quadVAO)
45 | :gl.drawArrays(:gl_const.gl_triangles(), 0, 6)
46 |
47 | :gl.bindVertexArray(0)
48 | end
49 |
50 | defp init_render_data(%__MODULE__{} = sprite) do
51 | [quadVAO] = :gl.genVertexArrays(1)
52 | sprite = %__MODULE__{sprite | quadVAO: quadVAO}
53 |
54 | vertices =
55 | Util.make_bits([
56 | 0.0,
57 | 1.0,
58 | 0.0,
59 | 1.0,
60 | 1.0,
61 | 0.0,
62 | 1.0,
63 | 0.0,
64 | 0.0,
65 | 0.0,
66 | 0.0,
67 | 0.0,
68 | 0.0,
69 | 1.0,
70 | 0.0,
71 | 1.0,
72 | 1.0,
73 | 1.0,
74 | 1.0,
75 | 1.0,
76 | 1.0,
77 | 0.0,
78 | 1.0,
79 | 0.0
80 | ])
81 |
82 | [vbo] = :gl.genBuffers(1)
83 |
84 | :gl.bindBuffer(:gl_const.gl_array_buffer(), vbo)
85 |
86 | :gl.bufferData(
87 | :gl_const.gl_array_buffer(),
88 | byte_size(vertices),
89 | vertices,
90 | :gl_const.gl_static_draw()
91 | )
92 |
93 | :gl.bindVertexArray(quadVAO)
94 | :gl.enableVertexAttribArray(0)
95 |
96 | :gl.vertexAttribPointer(
97 | 0,
98 | 4,
99 | :gl_const.gl_float(),
100 | :gl_const.gl_false(),
101 | 4 * byte_size(<<0::native-float-size(32)>>),
102 | 0
103 | )
104 |
105 | :gl.bindBuffer(:gl_const.gl_array_buffer(), 0)
106 | :gl.bindVertexArray(0)
107 |
108 | sprite
109 | end
110 | end
111 |
--------------------------------------------------------------------------------
/lib/breakout/renderer/text.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Renderer.Text do
2 | defstruct []
3 | end
4 |
--------------------------------------------------------------------------------
/lib/breakout/renderer/texture2d.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Renderer.Texture2D do
2 | alias Breakout.ImageParser
3 | require Logger
4 |
5 | @type t :: %__MODULE__{
6 | id: non_neg_integer(),
7 | width: non_neg_integer(),
8 | height: non_neg_integer(),
9 | internal_format: non_neg_integer(),
10 | image_format: non_neg_integer(),
11 | wrap_s: non_neg_integer(),
12 | wrap_t: non_neg_integer(),
13 | filter_min: non_neg_integer(),
14 | filter_max: non_neg_integer()
15 | }
16 |
17 | defstruct id: 0,
18 | width: 0,
19 | height: 0,
20 | internal_format: 0,
21 | image_format: 0,
22 | wrap_s: 0,
23 | wrap_t: 0,
24 | filter_min: 0,
25 | filter_max: 0
26 |
27 | @spec new() :: t()
28 | def new() do
29 | %__MODULE__{
30 | id: :gl.genTextures(1) |> hd,
31 | internal_format: :gl_const.gl_rgb(),
32 | image_format: :gl_const.gl_rgb(),
33 | wrap_s: :gl_const.gl_repeat(),
34 | wrap_t: :gl_const.gl_repeat(),
35 | filter_min: :gl_const.gl_linear(),
36 | filter_max: :gl_const.gl_linear()
37 | }
38 | end
39 |
40 | def load_gray(file) do
41 | tex = new()
42 | tex = %__MODULE__{tex | internal_format: :gl_const.gl_luminance, image_format: :gl_const.gl_luminance}
43 |
44 | data = case File.read(file) do
45 | {:ok, data} -> data
46 | {:error, err} -> Logger.error(err: err, file: file)
47 | end
48 |
49 | {:ok, image} = ImageParser.parse(data)
50 |
51 | generate(tex, image[:IHDR].width, image[:IHDR].height, image[:IDAT])
52 | end
53 |
54 | def load(file, alpha) do
55 | tex = new()
56 |
57 | tex =
58 | if alpha do
59 | %__MODULE__{tex | internal_format: :gl_const.gl_rgba(), image_format: :gl_const.gl_rgba()}
60 | else
61 | tex
62 | end
63 |
64 | # load file
65 | data =
66 | case File.read(file) do
67 | {:ok, data} -> data
68 | {:error, err} -> Logger.error(err: err, file: file)
69 | end
70 |
71 | {:ok, image} = ImageParser.parse(data)
72 |
73 | generate(tex, image[:IHDR].width, image[:IHDR].height, image[:IDAT])
74 | end
75 |
76 | @spec generate(
77 | texture :: t(),
78 | width :: non_neg_integer(),
79 | height :: non_neg_integer(),
80 | data :: binary() | 0
81 | ) :: t()
82 | def generate(texture, width, height, data) do
83 | tex = %__MODULE__{texture | width: width, height: height}
84 |
85 | :gl.bindTexture(:gl_const.gl_texture_2d(), tex.id)
86 |
87 | :gl.texImage2D(
88 | :gl_const.gl_texture_2d(),
89 | 0,
90 | tex.internal_format,
91 | width,
92 | height,
93 | 0,
94 | tex.image_format,
95 | :gl_const.gl_unsigned_byte(),
96 | data
97 | )
98 |
99 | :gl.texParameteri(:gl_const.gl_texture_2d(), :gl_const.gl_texture_wrap_s(), tex.wrap_s)
100 | :gl.texParameteri(:gl_const.gl_texture_2d(), :gl_const.gl_texture_wrap_t(), tex.wrap_t)
101 |
102 | :gl.texParameteri(
103 | :gl_const.gl_texture_2d(),
104 | :gl_const.gl_texture_min_filter(),
105 | tex.filter_min
106 | )
107 |
108 | :gl.texParameteri(
109 | :gl_const.gl_texture_2d(),
110 | :gl_const.gl_texture_mag_filter(),
111 | tex.filter_max
112 | )
113 |
114 | :gl.bindTexture(:gl_const.gl_texture_2d(), 0)
115 |
116 | tex
117 | end
118 |
119 | @spec bind(texture :: t()) :: :ok
120 | def bind(texture) do
121 | :gl.bindTexture(:gl_const.gl_texture_2d(), texture.id)
122 | :ok
123 | end
124 | end
125 |
--------------------------------------------------------------------------------
/lib/breakout/renderer/window.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Renderer.Window do
2 | alias Breakout.Input
3 |
4 | defstruct [:frame, :canvas, :context]
5 |
6 | @type t :: %__MODULE__{
7 | frame: :wxFrame.wxFrame(),
8 | canvas: :wxGLCanvas.wxGLCanvas(),
9 | context: :wxGLContext.wxGLContext()
10 | }
11 |
12 | @spec init(width :: pos_integer(), height :: pos_integer()) :: t()
13 | def init(width, height) do
14 | opts = [size: {width, height}]
15 |
16 | wx = :wx.new()
17 |
18 | frame = :wxFrame.new(wx, :wx_const.wx_id_any(), ~c"Elixir Breakout", opts)
19 |
20 | :wxWindow.connect(frame, :close_window)
21 |
22 | :wxFrame.show(frame)
23 |
24 | gl_attrib = [
25 | attribList: [
26 | :wx_const.wx_gl_core_profile(),
27 | :wx_const.wx_gl_major_version(),
28 | 4,
29 | :wx_const.wx_gl_minor_version(),
30 | 1,
31 | :wx_const.wx_gl_doublebuffer(),
32 | # :wx_const.wx_gl_depth_size, 24,
33 | :wx_const.wx_gl_sample_buffers(),
34 | 1,
35 | :wx_const.wx_gl_samples(),
36 | 4,
37 | 0
38 | ]
39 | ]
40 |
41 | canvas = :wxGLCanvas.new(frame, opts ++ gl_attrib)
42 | ctx = :wxGLContext.new(canvas)
43 |
44 | # cursor = :wxCursor.new(:wx_const.wx_cursor_blank)
45 | # :wxWindow.setCursor(canvas, cursor)
46 |
47 | # :wxWindow.captureMouse(canvas)
48 |
49 | :wxGLCanvas.setFocus(canvas)
50 |
51 | :wxGLCanvas.setCurrent(canvas, ctx)
52 |
53 | :wxGLCanvas.connect(canvas, :key_down, callback: &Input.handler/2)
54 | :wxGLCanvas.connect(canvas, :key_up, callback: &Input.handler/2)
55 | :wxGLCanvas.connect(canvas, :motion, callback: &Input.handler/2)
56 | :wxGLCanvas.connect(canvas, :mousewheel)
57 |
58 | %__MODULE__{
59 | frame: frame,
60 | canvas: canvas,
61 | context: ctx
62 | }
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/lib/breakout/resource_manager.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.ResourceManager do
2 | use GenServer
3 |
4 | require Logger
5 |
6 | alias Breakout.Renderer.{Shader, Texture2D}
7 |
8 | defstruct shaders: %{}, textures: %{}
9 |
10 | @type t :: %__MODULE__{
11 | shaders: %{atom() => Shader.t()},
12 | textures: %{atom() => Texture2D.t()}
13 | }
14 |
15 | def start_link(_arg) do
16 | GenServer.start_link(__MODULE__, [], name: __MODULE__)
17 | end
18 |
19 | @impl GenServer
20 | def init(_arg) do
21 | {:ok, new()}
22 | end
23 |
24 | def new() do
25 | %__MODULE__{}
26 | end
27 |
28 | @impl GenServer
29 | def handle_cast({:put_shader, name, shader}, state) do
30 | state = put_in(state.shaders[name], shader)
31 |
32 | {:noreply, state}
33 | end
34 |
35 | @impl GenServer
36 | def handle_cast({:put_texture, name, texture}, state) do
37 | state = put_in(state.textures[name], texture)
38 |
39 | {:noreply, state}
40 | end
41 |
42 | @impl GenServer
43 | def handle_call({:get_shader, name}, _from, state) do
44 | reply = Map.get(state.shaders, name)
45 |
46 | {:reply, {:ok, reply}, state}
47 | end
48 |
49 | @impl GenServer
50 | def handle_call({:get_texture, name}, _from, state) do
51 | reply = Map.get(state.textures, name)
52 |
53 | {:reply, {:ok, reply}, state}
54 | end
55 |
56 | def put_shader(shader, name) do
57 | GenServer.cast(__MODULE__, {:put_shader, name, shader})
58 |
59 | shader
60 | end
61 |
62 | def get_shader(name) do
63 | GenServer.call(__MODULE__, {:get_shader, name})
64 | end
65 |
66 | def put_texture(texture, name) do
67 | GenServer.cast(__MODULE__, {:put_texture, name, texture})
68 |
69 | texture
70 | end
71 |
72 | def get_texture(name) do
73 | GenServer.call(__MODULE__, {:get_texture, name})
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/lib/breakout/state.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.State do
2 | alias Breakout.PowerUp
3 | alias Breakout.Renderer.PostProcessor
4 | alias Breakout.ParticleGenerator
5 | alias Breakout.Renderer.Shader
6 | alias Breakout.BallObject
7 | alias Breakout.Renderer.Texture2D
8 | alias Breakout.GameObject
9 | alias Breakout.GameLevel
10 |
11 | @game_width 1200
12 | @game_height 800
13 |
14 | @type game_state :: :active | :menu | :win | nil
15 |
16 | @type t :: %__MODULE__{
17 | game_state: game_state(),
18 | keys: MapSet.t(integer()),
19 | keys_processed: MapSet.t(integer()),
20 | width: pos_integer(),
21 | height: pos_integer(),
22 | window: :wxWindow.wxWindow() | nil,
23 | t: pos_integer(),
24 | dt: non_neg_integer(),
25 | shader_program: non_neg_integer(),
26 | sprite_renderer: non_neg_integer(),
27 | levels: [GameLevel.t()],
28 | level: non_neg_integer(),
29 | player: GameObject.t(),
30 | background_texture: Texture2D.t(),
31 | ball: BallObject.t(),
32 | resources: %{shaders: %{atom() => Shader.t()}, textures: %{atom() => Texture2D.t()}},
33 | particle_generator: ParticleGenerator.t(),
34 | post_processor: PostProcessor.t(),
35 | elapsed: float(),
36 | start: float(),
37 | shake_time: float(),
38 | power_ups: [PowerUp.t()],
39 | font: :wxFont.wxFont(),
40 | brush: :wxBrush.wxBrush(),
41 | lives: pos_integer(),
42 | menu_string_size: {non_neg_integer(), non_neg_integer()},
43 | }
44 |
45 | defstruct [
46 | # :active | :menu | :win
47 | game_state: :active,
48 | keys: MapSet.new(),
49 | keys_processed: MapSet.new(),
50 | width: @game_width,
51 | height: @game_height,
52 | window: nil,
53 | t: 0,
54 | dt: 0,
55 | shader_program: 0,
56 | sprite_renderer: 0,
57 | levels: [],
58 | level: 0,
59 | player: GameObject.new(),
60 | background_texture: nil,
61 | ball: BallObject.new(),
62 | resources: %{shaders: %{}, textures: %{}},
63 | particle_generator: nil,
64 | post_processor: nil,
65 | elapsed: 0.0,
66 | start: 0.0,
67 | shake_time: 0.0,
68 | power_ups: [],
69 | font: nil,
70 | brush: nil,
71 | lives: 3,
72 | menu_string_size: {200, 200},
73 | ]
74 | end
75 |
--------------------------------------------------------------------------------
/lib/breakout/util.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.Util do
2 | def make_bits(list) do
3 | list
4 | |> Enum.reduce(<<>>, fn el, acc -> acc <> <> end)
5 | end
6 |
7 | def make_bits_unsigned(list) do
8 | list
9 | |> Enum.reduce(<<>>, fn el, acc -> acc <> <> end)
10 | end
11 |
12 | def to_priv(path) do
13 | :code.priv_dir(:breakout)
14 | |> Path.join(path)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/breakout/wx_records.ex:
--------------------------------------------------------------------------------
1 | defmodule Breakout.WxRecords do
2 | require Record
3 |
4 | for {type, record} <- Record.extract_all(from_lib: "wx/include/wx.hrl") do
5 | Record.defrecord(type, record)
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Breakout.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :breakout,
7 | version: "0.1.0",
8 | elixir: "~> 1.17",
9 | start_permanent: Mix.env() == :prod,
10 | deps: deps()
11 | ]
12 | end
13 |
14 | # Run "mix help compile.app" to learn about applications.
15 | def application do
16 | [
17 | extra_applications: [:logger, :wx, :observer, :runtime_tools, :tools, :xmerl, :debugger],
18 | mod: {Breakout.Application, []}
19 | ]
20 | end
21 |
22 | # Run "mix help deps" to learn about dependencies.
23 | defp deps do
24 | [
25 | {:membrane_core, "~> 1.1"},
26 | {:membrane_file_plugin, "~> 0.17.2"},
27 | {:membrane_portaudio_plugin, "~> 0.19.2"},
28 | {:membrane_ffmpeg_swresample_plugin, "~> 0.20.2"},
29 | {:membrane_mp3_mad_plugin, "~> 0.18.3"},
30 |
31 | {:membrane_funnel_plugin, "~> 0.9.0"},
32 | # {:exsync, "~> 0.4", only: :dev}
33 | # {:dep_from_hexpm, "~> 0.3.0"},
34 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
35 | ]
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "bimap": {:hex, :bimap, "1.3.0", "3ea4832e58dc83a9b5b407c6731e7bae87458aa618e6d11d8e12114a17afa4b3", [:mix], [], "hexpm", "bf5a2b078528465aa705f405a5c638becd63e41d280ada41e0f77e6d255a10b4"},
3 | "bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"},
4 | "bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"},
5 | "bundlex": {:hex, :bundlex, "1.5.3", "35d01e5bc0679510dd9a327936ffb518f63f47175c26a35e708cc29eaec0890b", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:qex, "~> 0.5", [hex: :qex, repo: "hexpm", optional: false]}, {:req, ">= 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:zarex, "~> 1.0", [hex: :zarex, repo: "hexpm", optional: false]}], "hexpm", "debd0eac151b404f6216fc60222761dff049bf26f7d24d066c365317650cd118"},
6 | "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
7 | "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"},
8 | "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"},
9 | "exsync": {:hex, :exsync, "0.4.1", "0a14fe4bfcb80a509d8a0856be3dd070fffe619b9ba90fec13c58b316c176594", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "cefb22aa805ec97ffc5b75a4e1dc54bcaf781e8b32564bf74abbe5803d1b5178"},
10 | "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
11 | "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
12 | "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
13 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
14 | "logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"},
15 | "membrane_common_c": {:hex, :membrane_common_c, "0.16.0", "caf3f29d2f5a1d32d8c2c122866110775866db2726e4272be58e66dfdf4bce40", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "a3c7e91de1ce1f8b23b9823188a5d13654d317235ea0ca781c05353ed3be9b1c"},
16 | "membrane_core": {:hex, :membrane_core, "1.1.1", "4dcff6e9f3b2ecd4f437c20e201e53957731772c0f15b3005062c41f7f58f500", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:qex, "~> 0.3", [hex: :qex, repo: "hexpm", optional: false]}, {:ratio, "~> 3.0 or ~> 4.0", [hex: :ratio, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3802f3fc071505c59d48792487d9927e803d4edb4039710ffa52cdb60bb0aecc"},
17 | "membrane_ffmpeg_swresample_plugin": {:hex, :membrane_ffmpeg_swresample_plugin, "0.20.2", "2e669f0b25418d10b51a73bc52d2e12e4a3a26b416c5c1199d852c3f781a18b3", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.2", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_common_c, "~> 0.16.0", [hex: :membrane_common_c, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:membrane_raw_audio_format, "~> 0.12.0", [hex: :membrane_raw_audio_format, repo: "hexpm", optional: false]}, {:mockery, "~> 2.1", [hex: :mockery, repo: "hexpm", optional: false]}, {:unifex, "~> 1.1", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "6c8d3bcd61d568dd94cabb9b45f29e8926e0076e4432d8f419378e004e02147c"},
18 | "membrane_file_plugin": {:hex, :membrane_file_plugin, "0.17.2", "650e134c2345d946f930082fac8bac9f5aba785a7817d38a9a9da41ffc56fa92", [:mix], [{:logger_backends, "~> 1.0", [hex: :logger_backends, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "df50c6040004cd7b901cf057bd7e99c875bbbd6ae574efc93b2c753c96f43b9d"},
19 | "membrane_funnel_plugin": {:hex, :membrane_funnel_plugin, "0.9.0", "9cfe09e44d65751f7d9d8d3c42e14797f7be69e793ac112ea63cd224af70a7bf", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "988790aca59d453a6115109f050699f7f45a2eb6a7f8dc5c96392760cddead54"},
20 | "membrane_mp3_mad_plugin": {:hex, :membrane_mp3_mad_plugin, "0.18.3", "7cf02b7185439918f2deb794af53c13dbc07a7cf61ff81500731b4cf1d62eb5b", [:mix], [{:bundlex, "~> 1.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_common_c, "~> 0.16.0", [hex: :membrane_common_c, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_mpegaudio_format, "~> 0.3.0", [hex: :membrane_mpegaudio_format, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:membrane_raw_audio_format, "~> 0.12.0", [hex: :membrane_raw_audio_format, repo: "hexpm", optional: false]}, {:unifex, "~> 1.1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "0917fb3aa33529e331ce2d363f2479e0efc648ec791ceed1c2ce7f54139bf9ca"},
21 | "membrane_mpegaudio_format": {:hex, :membrane_mpegaudio_format, "0.3.0", "d4fee77fad9f953171c52acd6d53b6646cfc7fbb827c63caa7c6a1efeb86450a", [:mix], [], "hexpm", "dec903efd0086133402b44515d04301790832b4f39995747b0e712c8f966d50d"},
22 | "membrane_portaudio_plugin": {:hex, :membrane_portaudio_plugin, "0.19.2", "d67e1aba624f7b87c4932b2016082d2be5740d537429ac37330e03dacead37e6", [:mix], [{:bunch, "~> 1.5", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.5", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_common_c, "~> 0.16.0", [hex: :membrane_common_c, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:membrane_raw_audio_format, "~> 0.12.0", [hex: :membrane_raw_audio_format, repo: "hexpm", optional: false]}, {:mockery, "~> 2.1", [hex: :mockery, repo: "hexpm", optional: false]}], "hexpm", "216adf08fbc9c0cd0cee791a25a44fa2da1a90537bd4fdb0457b6701ea2b0715"},
23 | "membrane_precompiled_dependency_provider": {:hex, :membrane_precompiled_dependency_provider, "0.1.2", "8af73b7dc15ba55c9f5fbfc0453d4a8edfb007ade54b56c37d626be0d1189aba", [:mix], [{:bundlex, "~> 1.4", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "7fe3e07361510445a29bee95336adde667c4162b76b7f4c8af3aeb3415292023"},
24 | "membrane_raw_audio_format": {:hex, :membrane_raw_audio_format, "0.12.0", "b574cd90f69ce2a8b6201b0ccf0826ca28b0fbc8245b8078d9f11cef65f7d5d5", [:mix], [{:bimap, "~> 1.1", [hex: :bimap, repo: "hexpm", optional: false]}, {:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "6e6c98e3622a2b9df19eab50ba65d7eb45949b1ba306fa8423df6cdb12fd0b44"},
25 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
26 | "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
27 | "mockery": {:hex, :mockery, "2.3.3", "3dba87bd0422a513e6af6e0d811383f38f82ac6be5d3d285a5fcca9c299bd0ac", [:mix], [], "hexpm", "17282be00613286254298117cd25e607a39f15ac03b41c631f60e52f5b5ec974"},
28 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
29 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
30 | "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"},
31 | "qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"},
32 | "ratio": {:hex, :ratio, "4.0.1", "3044166f2fc6890aa53d3aef0c336f84b2bebb889dc57d5f95cc540daa1912f8", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "c60cbb3ccdff9ffa56e7d6d1654b5c70d9f90f4d753ab3a43a6bf40855b881ce"},
33 | "req": {:hex, :req, "0.5.6", "8fe1eead4a085510fe3d51ad854ca8f20a622aae46e97b302f499dfb84f726ac", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cfaa8e720945d46654853de39d368f40362c2641c4b2153c886418914b372185"},
34 | "shmex": {:hex, :shmex, "0.5.1", "81dd209093416bf6608e66882cb7e676089307448a1afd4fc906c1f7e5b94cf4", [:mix], [{:bunch_native, "~> 0.5.0", [hex: :bunch_native, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "c29f8286891252f64c4e1dac40b217d960f7d58def597c4e606ff8fbe71ceb80"},
35 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
36 | "unifex": {:hex, :unifex, "1.1.2", "ed3366515b6612a5a08d24a38658dea18a6c6001e79cf41e3a2edd07004d3c6d", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.4", [hex: :bundlex, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}], "hexpm", "c25b9d4d1a1c76716ecdf68d0873553fdab4105f418ef76f646a5cb47e0396ab"},
37 | "zarex": {:hex, :zarex, "1.0.5", "58239e3ee5d75f343262bb4df5cf466555a1c689f920e5d3651a9333972f7c7e", [:mix], [], "hexpm", "9fb72ef0567c2b2742f5119a1ba8a24a2fabb21b8d09820aefbf3e592fa9a46a"},
38 | }
39 |
--------------------------------------------------------------------------------
/priv/audio/block.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/audio/block.mp3
--------------------------------------------------------------------------------
/priv/audio/breakout.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/audio/breakout.mp3
--------------------------------------------------------------------------------
/priv/audio/paddle.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/audio/paddle.mp3
--------------------------------------------------------------------------------
/priv/audio/powerup.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/audio/powerup.mp3
--------------------------------------------------------------------------------
/priv/audio/solid.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/audio/solid.mp3
--------------------------------------------------------------------------------
/priv/levels/four.lvl:
--------------------------------------------------------------------------------
1 | 1 2 1 2 1 2 1 2 1 2 1 2 1
2 | 2 2 2 2 2 2 2 2 2 2 2 2 2
3 | 2 1 3 1 4 1 5 1 4 1 3 1 2
4 | 2 3 3 4 4 5 5 5 4 4 3 3 2
5 | 2 1 3 1 4 1 5 1 4 1 3 1 2
6 | 2 2 3 3 4 4 5 4 4 3 3 2 2
7 |
--------------------------------------------------------------------------------
/priv/levels/one.lvl:
--------------------------------------------------------------------------------
1 | 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5
2 | 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5
3 | 4 4 4 4 4 0 0 0 0 0 4 4 4 4 4
4 | 4 1 4 1 4 0 0 1 0 0 4 1 4 1 4
5 | 3 3 3 3 3 0 0 0 0 0 3 3 3 3 3
6 | 3 3 1 3 3 3 3 3 3 3 3 3 1 3 3
7 | 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
8 | 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
--------------------------------------------------------------------------------
/priv/levels/three.lvl:
--------------------------------------------------------------------------------
1 | 0 0 0 0 0 0 0 0 0 0 0 0 0
2 | 0 0 2 0 0 0 0 0 0 0 2 0 0
3 | 0 0 0 2 0 0 0 0 0 2 0 0 0
4 | 0 0 0 5 5 5 5 5 5 5 0 0 0
5 | 0 0 5 5 0 5 5 5 0 5 5 0 0
6 | 0 5 5 5 5 5 5 5 5 5 5 5 0
7 | 0 3 0 1 1 1 1 1 1 1 0 3 0
8 | 0 3 0 3 0 0 0 0 0 3 0 3 0
9 | 0 0 0 0 4 4 0 4 4 0 0 0 0
10 |
--------------------------------------------------------------------------------
/priv/levels/two.lvl:
--------------------------------------------------------------------------------
1 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
2 | 1 0 5 5 0 5 5 0 5 5 0 5 5 0 1
3 | 1 5 5 5 5 5 5 5 5 5 5 5 5 5 1
4 | 1 0 3 3 0 3 3 0 3 3 0 3 3 0 1
5 | 1 3 3 3 3 3 3 3 3 3 3 3 3 3 1
6 | 1 0 2 2 0 2 2 0 2 2 0 2 2 0 1
7 | 1 2 2 2 2 2 2 2 2 2 2 2 2 2 1
8 | 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1
9 |
--------------------------------------------------------------------------------
/priv/shaders/particle/fragment.fs:
--------------------------------------------------------------------------------
1 | #version 410 core
2 |
3 | in vec2 TexCoords;
4 | in vec4 ParticleColor;
5 | out vec4 color;
6 |
7 | uniform sampler2D sprite;
8 |
9 | void main() {
10 | color = (texture(sprite, TexCoords) * ParticleColor);
11 | }
--------------------------------------------------------------------------------
/priv/shaders/particle/vertex.vs:
--------------------------------------------------------------------------------
1 | #version 410 core
2 | layout (location = 0) in vec4 vertex;
3 |
4 | out vec2 TexCoords;
5 | out vec4 ParticleColor;
6 |
7 | uniform mat4 projection;
8 | uniform vec2 offset;
9 | uniform vec4 color;
10 |
11 | void main() {
12 | float scale = 10.0f;
13 | TexCoords = vertex.zw;
14 | ParticleColor = color;
15 | gl_Position = projection * vec4((vertex.xy * scale) + offset, 0.0, 1.0);
16 | }
--------------------------------------------------------------------------------
/priv/shaders/post_processor/fragment.fs:
--------------------------------------------------------------------------------
1 | #version 410 core
2 | in vec2 TexCoords;
3 | out vec4 color;
4 |
5 | uniform sampler2D scene;
6 | uniform vec2 offsets[9];
7 | uniform int edge_kernel[9];
8 | uniform float blur_kernel[9];
9 |
10 | uniform bool chaos;
11 | uniform bool confuse;
12 | uniform bool shake;
13 |
14 | #define SIZE 9
15 |
16 | void main() {
17 | color = vec4(0.0f); // , 0.0f, 0.0f, 1.0f);
18 | vec3 samplex[9];
19 |
20 | if (chaos || shake) {
21 | for (int i = 0; i < 9; i++) {
22 | samplex[i] = vec3(texture(scene, TexCoords.st + offsets[i]));
23 | }
24 | }
25 |
26 | if (chaos) {
27 | for (int i = 0; i < 9; i++) {
28 | color += vec4(samplex[i] * edge_kernel[i], 0.0f);
29 | }
30 |
31 | color.a = 1.0f;
32 | } else if (confuse) {
33 | color = vec4(1.0 - texture(scene, TexCoords).rgb, 1.0);
34 | } else if (shake) {
35 | for (int i = 0; i < 9; i++) {
36 | color += vec4(samplex[i] * blur_kernel[i], 0.0f);
37 | }
38 | color.a = 1.0f;
39 | } else {
40 | color = texture(scene, TexCoords);
41 | }
42 | }
--------------------------------------------------------------------------------
/priv/shaders/post_processor/vertex.vs:
--------------------------------------------------------------------------------
1 | #version 410 core
2 | layout (location = 0) in vec4 vertex;
3 |
4 | out vec2 TexCoords;
5 |
6 | uniform bool chaos;
7 | uniform bool confuse;
8 | uniform bool shake;
9 | uniform float time;
10 |
11 | void main() {
12 | gl_Position = vec4(vertex.xy, 0.0f, 1.0f);
13 | vec2 texture = vertex.zw;
14 | if (chaos) {
15 | float strength = 0.3;
16 | vec2 pos = vec2(texture.x + sin(time) * strength, texture.y + cos(time) * strength);
17 | TexCoords = pos;
18 | } else if (confuse) {
19 | TexCoords = vec2(1.0 - texture.x, 1.0 - texture.y);
20 | } else {
21 | TexCoords = texture;
22 | }
23 |
24 | if (shake) {
25 | float strength = 0.01;
26 | gl_Position.x += cos(time * 10) * strength;
27 | gl_Position.y += cos(time * 15) * strength;
28 | }
29 | }
--------------------------------------------------------------------------------
/priv/shaders/sprite/fragment.fs:
--------------------------------------------------------------------------------
1 | #version 410 core
2 |
3 | in vec2 TexCoords;
4 | out vec4 color;
5 |
6 | uniform sampler2D image;
7 | uniform vec3 spriteColor;
8 |
9 | void main()
10 | {
11 | color = vec4(spriteColor, 1.0) * texture(image, TexCoords);
12 | }
--------------------------------------------------------------------------------
/priv/shaders/sprite/vertex.vs:
--------------------------------------------------------------------------------
1 | #version 410 core
2 |
3 | layout (location = 0) in vec4 vertex; //
4 |
5 | out vec2 TexCoords;
6 |
7 | uniform mat4 model;
8 | uniform mat4 projection;
9 |
10 | void main()
11 | {
12 | TexCoords = vertex.zw;
13 | gl_Position = projection * model * vec4(vertex.xy, 0.0, 1.0);
14 | }
--------------------------------------------------------------------------------
/priv/sprites/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/sprites/test.png
--------------------------------------------------------------------------------
/priv/textures/ascii.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/ascii.png
--------------------------------------------------------------------------------
/priv/textures/ascii_rgb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/ascii_rgb.png
--------------------------------------------------------------------------------
/priv/textures/awesomeface.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/awesomeface.png
--------------------------------------------------------------------------------
/priv/textures/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/background.jpg
--------------------------------------------------------------------------------
/priv/textures/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/background.png
--------------------------------------------------------------------------------
/priv/textures/block.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/block.png
--------------------------------------------------------------------------------
/priv/textures/block_solid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/block_solid.png
--------------------------------------------------------------------------------
/priv/textures/paddle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/paddle.png
--------------------------------------------------------------------------------
/priv/textures/particle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/particle.png
--------------------------------------------------------------------------------
/priv/textures/powerup_chaos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/powerup_chaos.png
--------------------------------------------------------------------------------
/priv/textures/powerup_confuse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/powerup_confuse.png
--------------------------------------------------------------------------------
/priv/textures/powerup_increase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/powerup_increase.png
--------------------------------------------------------------------------------
/priv/textures/powerup_passthrough.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/powerup_passthrough.png
--------------------------------------------------------------------------------
/priv/textures/powerup_speed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/powerup_speed.png
--------------------------------------------------------------------------------
/priv/textures/powerup_sticky.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/powerup_sticky.png
--------------------------------------------------------------------------------
/src/gl_const.erl:
--------------------------------------------------------------------------------
1 | -module(gl_const).
2 | -compile(nowarn_export_all).
3 | -compile(export_all).
4 |
5 | -include_lib("wx/include/gl.hrl").
6 |
7 | gl_depth_test() -> ?GL_DEPTH_TEST.
8 |
9 | gl_lequal() -> ?GL_LEQUAL.
10 | gl_color_buffer_bit() -> ?GL_COLOR_BUFFER_BIT.
11 |
12 | gl_depth_buffer_bit() -> ?GL_DEPTH_BUFFER_BIT.
13 |
14 | gl_triangles() -> ?GL_TRIANGLES.
15 | gl_array_buffer() -> ?GL_ARRAY_BUFFER.
16 | gl_element_array_buffer() -> ?GL_ELEMENT_ARRAY_BUFFER.
17 | gl_static_draw() -> ?GL_STATIC_DRAW.
18 |
19 | gl_vertex_shader() -> ?GL_VERTEX_SHADER.
20 | gl_fragment_shader() -> ?GL_FRAGMENT_SHADER.
21 |
22 | gl_compile_status() -> ?GL_COMPILE_STATUS.
23 | gl_link_status() -> ?GL_LINK_STATUS.
24 |
25 | gl_float() -> ?GL_FLOAT.
26 | gl_false() -> ?GL_FALSE.
27 | gl_true() -> ?GL_TRUE.
28 | gl_unsigned_int() -> ?GL_UNSIGNED_INT.
29 | gl_unsigned_byte() -> ?GL_UNSIGNED_BYTE.
30 |
31 | gl_front_and_back() -> ?GL_FRONT_AND_BACK.
32 | gl_line() -> ?GL_LINE.
33 | gl_fill() -> ?GL_FILL.
34 | gl_debug_output() -> ?GL_DEBUG_OUTPUT.
35 | gl_texture_2d() -> ?GL_TEXTURE_2D.
36 | gl_texture_wrap_s() -> ?GL_TEXTURE_WRAP_S.
37 | gl_texture_wrap_t() -> ?GL_TEXTURE_WRAP_T.
38 | gl_texture_min_filter() -> ?GL_TEXTURE_MIN_FILTER.
39 | gl_texture_mag_filter() -> ?GL_TEXTURE_MAG_FILTER.
40 | gl_rgb() -> ?GL_RGB.
41 | gl_rgba() -> ?GL_RGBA.
42 | gl_multisample() -> ?GL_MULTISAMPLE.
43 | gl_luminance() -> ?GL_LUMINANCE.
44 |
45 | gl_texture0() -> ?GL_TEXTURE0.
46 |
47 | gl_cull_face() -> ?GL_CULL_FACE.
48 | gl_back() -> ?GL_BACK.
49 | gl_front() -> ?GL_FRONT.
50 | gl_ccw() -> ?GL_CCW.
51 | gl_cw() -> ?GL_CW.
52 |
53 | gl_info_log_length() -> ?GL_INFO_LOG_LENGTH.
54 |
55 | gl_blend() -> ?GL_BLEND.
56 | gl_src_alpha() -> ?GL_SRC_ALPHA.
57 | gl_one() -> ?GL_ONE.
58 | gl_one_minus_src_alpha() -> ?GL_ONE_MINUS_SRC_ALPHA.
59 |
60 | gl_repeat() -> ?GL_REPEAT.
61 | gl_linear() -> ?GL_LINEAR.
62 | gl_nearest() -> ?GL_NEAREST.
63 |
64 | gl_framebuffer() -> ?GL_FRAMEBUFFER.
65 | gl_renderbuffer() -> ?GL_RENDERBUFFER.
66 | gl_color_attachment0() -> ?GL_COLOR_ATTACHMENT0.
67 | gl_framebuffer_complete() -> ?GL_FRAMEBUFFER_COMPLETE.
68 | gl_read_framebuffer() -> ?GL_READ_FRAMEBUFFER.
69 | gl_draw_framebuffer() -> ?GL_DRAW_FRAMEBUFFER.
70 |
71 | gl_texture_env() -> ?GL_TEXTURE_ENV.
72 | gl_texture_env_mode() -> ?GL_TEXTURE_ENV_MODE.
73 | gl_replace() -> ?GL_REPLACE.
--------------------------------------------------------------------------------
/src/wx_const.erl:
--------------------------------------------------------------------------------
1 | -module(wx_const).
2 | -compile(nowarn_export_all).
3 | -compile(export_all).
4 |
5 | -include_lib("wx/include/wx.hrl").
6 |
7 | wx_id_any() -> ?wxID_ANY.
8 | wx_gl_rgba() -> ?WX_GL_RGBA.
9 |
10 | wx_gl_doublebuffer() -> ?WX_GL_DOUBLEBUFFER.
11 | wx_gl_depth_size() -> ?WX_GL_DEPTH_SIZE.
12 | wx_gl_forward_compat() -> ?WX_GL_FORWARD_COMPAT.
13 |
14 | wxk_left() -> ?WXK_LEFT.
15 | wxk_right() -> ?WXK_RIGHT.
16 | wxk_up() -> ?WXK_UP.
17 | wxk_down() -> ?WXK_DOWN.
18 | wxk_space() -> ?WXK_SPACE.
19 | wxk_raw_control() -> ?WXK_RAW_CONTROL.
20 |
21 | wx_gl_major_version() -> ?WX_GL_MAJOR_VERSION.
22 |
23 | wx_gl_minor_version() -> ?WX_GL_MINOR_VERSION.
24 |
25 | wx_gl_core_profile() -> ?WX_GL_CORE_PROFILE.
26 | wx_gl_sample_buffers() -> ?WX_GL_SAMPLE_BUFFERS.
27 |
28 | wx_gl_samples() -> ?WX_GL_SAMPLES.
29 |
30 | wx_null_cursor() -> ?wxNullCursor.
31 | wx_cursor_blank() -> ?wxCURSOR_BLANK.
32 | wx_cursor_cross() -> ?wxCURSOR_CROSS.
33 |
34 | wx_fontfamily_default() -> ?wxFONTFAMILY_DEFAULT.
35 | wx_fontfamily_teletype() -> ?wxFONTFAMILY_TELETYPE.
36 | wx_normal() -> ?wxNORMAL.
37 | wx_fontstyle_normal() -> ?wxFONTSTYLE_NORMAL.
38 | wx_fontweight_bold() -> ?wxFONTWEIGHT_BOLD.
39 | wx_fontweight_normal() -> ?wxFONTWEIGHT_NORMAL.
--------------------------------------------------------------------------------
/test/breakout_test.exs:
--------------------------------------------------------------------------------
1 | defmodule BreakoutTest do
2 | use ExUnit.Case
3 | doctest Breakout
4 | end
5 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------