Updates, various.

- Naka has been split to a separate file for better readability.
  - Naka::Noise2D added. Basically Perlin noise, but it adds a few APIs
    to sum stacks of octaves and such. Generation from seed is still
    missing even though it’s critical.
  - An example map is generated using several Naka::Noise2D.
  - Some assets added, and used to display an example map. Objects that
    are higher than one tile are displayed, but widths can’t overflow
    at the moment.
  - Maps are saved to disk. This is still minimal, however, and loading
    them from save files is not implemented at the moment.
  - Some Naka::Event were added at some point, although they’re still
    very much works in progress, and probably incomplete.
  - Maps are displayed in a very crude way. Transitions between terrain
    types are unsupported, for example.
This commit is contained in:
Luka Vandervelden 2018-11-04 00:32:03 +09:00
parent 04c5e35f52
commit 9116227d70
15 changed files with 529 additions and 130 deletions

BIN
assets/cactus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
assets/dirt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
assets/flowers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
assets/grass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
assets/haunted.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
assets/rocks.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
assets/sand.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
assets/stone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
assets/tiles.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
assets/tree.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
assets/tree_pine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
assets/water.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,167 +1,211 @@
require "sdl"
require "sdl/image"
require "json"
## Helper code comes here
require "./naka.cr"
class Naka
def self.init
SDL.init SDL::Init::VIDEO
SDL::IMG.init SDL::IMG::Init::PNG
class Map
@save_directory : String
getter altitude_maps : Naka::Noise2D
getter humidity_maps : Naka::Noise2D
getter evil_maps : Naka::Noise2D
getter flowers_map : Naka::Noise2D
def initialize(@save_directory)
Dir.mkdir_p @save_directory
at_exit { SDL.quit }
end
@altitude_maps = Naka::Noise2D.new.tap do |a|
a.add_octave(32, 1)
a.add_octave(16, 0.5)
a.add_octave(8, 0.25)
a.add_octave(4, 0.125)
a.add_octave(2, 1/16) # wtf, too smol
end
class Naka::Renderer < SDL::Renderer
@humidity_maps = Naka::Noise2D.new.tap do |a|
a.add_octave(64, 1)
a.add_octave(32, 0.5)
a.add_octave(16, 0.25)
a.add_octave(8, 0.20)
a.add_octave(4, 0.15)
a.add_octave(2, 0.10)
end
class Naka::Window
alias Flags = LibSDL::WindowFlags
alias Position = LibSDL::WindowPosition
getter renderer
def initialize(title, width, height,
x : Position = Position::UNDEFINED,
y : Position = Position::UNDEFINED,
flags : Flags = Flags::SHOWN)
@window = SDL::Window.new(title, width, height, x, y, flags)
@renderer = Naka::Renderer.new @window
@evil_maps = Naka::Noise2D.new.tap do |a|
a.add_octave(48, 0.6)
a.add_octave(32, 0.6)
a.add_octave(16, 0.3)
end
def newImage(file_path) : SDL::Texture
SDL::IMG.load file_path, @renderer
@flowers_map = Naka::Noise2D.new(1, 1)
end
# FIXME: Well probably want options for scaling, rotations, and so on.
def draw(texture : SDL::Texture, x : Int32, y : Int32)
@renderer.copy texture,
dstrect: SDL::Rect[x, y, texture.width, texture.height]
end
class Object
getter type : String
def initialize(@type)
end
class Naka::Timer
getter start
getter last_update
def initialize
@start = Time.now
@last_update = @start
end
def step
now = Time.now
dt = now.epoch_ms - @last_update.epoch_ms
@last_update = now
dt
end
end
class Naka::Event
class Quit
end
class Draw
getter window : Naka::Window
def initialize(@window)
end
end
class Update
getter dt : Int64
def initialize(@dt)
end
end
# FIXME: Split in KeyUp and KeyDown.
class KeyUp
getter key : LibSDL::Keycode
getter scan_code : LibSDL::Scancode
def initialize(@key, @scan_code)
end
end
class KeyDown
getter key : LibSDL::Keycode
getter scan_code : LibSDL::Scancode
getter repeat : Bool
def initialize(@key, @scan_code, @repeat)
end
end
# FIXME: THIS MAIN LOOP IS **NOT** READY FOR PRODUCTION
# Update events are still missing, and both Draw and Update should
# be time-based.
# Why having a `window` parameter instead of making a Window#loop method?
# Because we may want to have a `windows` parameter in the future, although
# unlikely. But still possible.
def self.loop(window, &block)
max_fps = 60
max_dt = 1000. / 60
timer = Naka::Timer.new
renderer = window.renderer
::loop do
exit_requested = false
while event = SDL::Event.poll
r_value = yield case event
when SDL::Event::Quit
Naka::Event::Quit.new
when SDL::Event::Keyboard
keysym = event.keysym
if event.type == LibSDL::EventType::KEYUP
Naka::Event::KeyUp.new keysym.sym, keysym.scancode
elsif event.type == LibSDL::EventType::KEYDOWN
Naka::Event::KeyDown.new keysym.sym, keysym.scancode, event.repeat != 0
end
else # FIXME: Most event types may need proper Naka:: classes.
event
end
if r_value == Naka::Event::Quit
exit_requested = true
break
end
end
break if exit_requested
dt = timer.step
r_value = yield Naka::Event::Update.new dt
break if r_value == Naka::Event::Quit
renderer.draw_color = SDL::Color[255, 255, 255, 255]
renderer.clear
yield Naka::Event::Draw.new window
renderer.present
sleep 0.001
def to_json(builder)
builder.object do
builder.field "type", @type
end
end
end
## Actual application code comes here
class Tile < Array(Object)
def initialize(element)
initialize
self << element
end
end
CHUNK_SIZE = 32
class Chunk
property tiles : Array(Array(Tile))
getter x : Int32
getter y : Int32
def initialize(@x, @y, map)
p "Creating Map::Chunk"
@tiles = Array.new(CHUNK_SIZE) do |x|
x += @x * CHUNK_SIZE
Array.new(CHUNK_SIZE) do |y|
y += @y * CHUNK_SIZE
Tile.new(Object.new "stone").tap do |i|
altitude = map.altitude_maps.get(x, y) + 0.5
humidity = map.humidity_maps.get(x, y) + 0.5
evil = map.evil_maps.get(x, y)
if altitude < 0.25
i << Object.new "sand"
if altitude < 0.175
i << Object.new "water"
else
if humidity < 0.33
i << Object.new "sand"
else
i << Object.new "dirt"
end
end
elsif altitude < 0.75
i << Object.new "dirt"
if evil > 0.5
i << Object.new "haunted"
else
i << Object.new "grass"
end
if altitude > 0.35 && altitude < 0.65
if humidity > 0.4
if humidity > 0.8
i << Object.new "tree"
elsif humidity < 0.6
i << Object.new "pine"
else
if Random.rand(2) == 0
i << Object.new "tree"
else
i << Object.new "pine"
end
end
else
if Random.rand(10) == 0
i << Object.new "flowers"
end
end
end
elsif altitude > 0.85
i << Object.new "rocks"
end
end
end
end
end
def to_json
{
:x => @x,
:y => @y,
:tiles => @tiles
}.to_json
end
def get(x, y)
@tiles[x][y]
end
end
@chunks = Hash(Int32, Hash(Int32, Chunk)).new
def get_chunk(x, y)
chunks_list = @chunks[x]?
if chunks_list.nil?
chunks_list = Hash(Int32, Chunk).new
@chunks[x] = chunks_list
end
chunk = chunks_list[y]?
if chunk.nil?
chunk = Chunk.new x, y, self
chunks_list[y] = chunk
save chunk
end
chunk
end
def save(chunk : Chunk)
File.write "#{@save_directory}/chunk-#{chunk.x}-#{chunk.y}.json", chunk.to_json
end
def get(x, y)
get_chunk(x / CHUNK_SIZE, y / CHUNK_SIZE).get(x % CHUNK_SIZE, y % CHUNK_SIZE)
end
end
class ImagesLoader
getter grass : SDL::Texture
getter stone : SDL::Texture
getter dirt : SDL::Texture
getter tree : SDL::Texture
getter pine : SDL::Texture
getter flowers : SDL::Texture
getter sand : SDL::Texture
getter water : SDL::Texture
getter haunted : SDL::Texture
getter rocks : SDL::Texture
def initialize(window)
@grass = window.newImage "assets/grass.png"
@stone = window.newImage "assets/stone.png"
@dirt = window.newImage "assets/dirt.png"
@tree = window.newImage "assets/tree.png"
@flowers = window.newImage "assets/flowers.png"
@sand = window.newImage "assets/sand.png"
@water = window.newImage "assets/water.png"
@haunted = window.newImage "assets/haunted.png"
@pine = window.newImage "assets/tree_pine.png"
@rocks = window.newImage "assets/rocks.png"
end
end
Naka.init
window = Naka::Window.new "Test", 640, 480
window = Naka::Window.new "Test", 16*128, 16*128, flags: LibSDL::WindowFlags::RESIZABLE | LibSDL::WindowFlags::SHOWN | LibSDL::WindowFlags::OPENGL
naka = window.newImage "naka.png"
images = ImagesLoader.new window
connection_font = window.newFont "Connection.otf", 30.5
window.set_font connection_font
map = Map.new "map-test-01"
Naka::Event.loop window do |event|
case event
@ -169,7 +213,54 @@ Naka::Event.loop window do |event|
next Naka::Event::Quit
when Naka::Event::Update
when Naka::Event::Draw
event.window.draw naka, 20, 20
zoom_level = 1.0
128.times do |x|
128.times do |y|
tile = map.get x, y
# FIXME:
# - Please dont draw more than one layer of ground…
# - Transitions. (eg. between dirt and grass)
tile.each do |object|
image = case object.type
when "grass"
images.grass
when "dirt"
images.dirt
when "stone"
images.stone
when "tree"
images.tree
when "pine"
images.pine
when "flowers"
images.flowers
when "water"
images.water
when "haunted"
images.haunted
when "sand"
images.sand
when "rocks"
images.rocks
else
images.stone
end
window.draw(
image,
x: (x * 16 * zoom_level).to_i,
y: (y * 16 * zoom_level - (image.height - 16) * zoom_level).to_i,
scale_x: zoom_level,
scale_y: zoom_level
)
end
end
end
# Urgh. This shit eats CPU. Fonts need goddamn fixing.
#window.print "The quick brown fox jumps over the lazy dog.", 50, 50
when Naka::Event::KeyUp, Naka::Event::KeyDown
pp! event
end

203
src/naka.cr Normal file
View File

@ -0,0 +1,203 @@
require "sdl"
require "sdl/image"
require "sdl/ttf"
class Naka
end
require "./noise.cr"
## Helper code comes here
class Naka
def self.init
SDL.init SDL::Init::VIDEO
SDL::IMG.init SDL::IMG::Init::PNG
SDL::TTF.init
at_exit {
SDL.quit
SDL::TTF.quit
}
end
end
class Naka::Renderer < SDL::Renderer
end
class Naka::Window
alias Flags = LibSDL::WindowFlags
alias Position = LibSDL::WindowPosition
getter renderer
@current_font : SDL::TTF::Font? = nil
def initialize(title, width, height,
x : Position = Position::UNDEFINED,
y : Position = Position::UNDEFINED,
flags : Flags = Flags::SHOWN)
@window = SDL::Window.new(title, width, height, x, y, flags)
@renderer = Naka::Renderer.new @window
@renderer.draw_blend_mode = SDL::BlendMode::BLEND
end
def newImage(file_path) : SDL::Texture
SDL::IMG.load(file_path, @renderer).tap do |texture|
texture.blend_mode = SDL::BlendMode::BLEND
end
end
def newFont(file_path, font_size = 12) : SDL::TTF::Font
SDL::TTF::Font.new file_path, font_size
end
# FIXME: Well probably want options for scaling, rotations, and so on.
def draw(texture : SDL::Texture, x : Int32, y : Int32, scale_x = 1, scale_y = 1)
@renderer.copy texture,
dstrect: SDL::Rect[x, y, (texture.width * scale_x).to_i, (texture.height * scale_y).to_i]
end
def draw(surface : SDL::Surface, x : Int32, y : Int32)
@renderer.copy surface,
dstrect: SDL::Rect[x, y, surface.width, surface.height]
end
def set_font(@current_font)
end
# FIXME:
# - This only prints properly “solid” text. Shaded text (depixelated)
# would need some additional OpenGL manipulations.
# - This is very inefficient resources-wise. If we are willing to drop
# ligatures support (is it even supported in SDL::TTF?), we should
# build a textures atlas of all glyphs and use that to render text.
# - Alternatively, it may be worth it to let users generate a texture of
# some text and draw it, manually.
def print(text, x, y)
font = @current_font
return if font.nil?
surface = font.render_solid(text, SDL::Color[0, 0, 0, 255], @renderer.draw_color)
draw surface, x, y
end
end
class Naka::Timer
getter start
getter last_update
def initialize
@start = Time.now
@last_update = @start
end
def step
now = Time.now
dt = now.epoch_ms - @last_update.epoch_ms
@last_update = now
dt
end
end
class Naka::Event
class Quit
end
class Draw
getter window : Naka::Window
def initialize(@window)
end
end
class Update
getter dt : Int64
def initialize(@dt)
end
end
# FIXME: Split in KeyUp and KeyDown.
class KeyUp
getter key : LibSDL::Keycode
getter scan_code : LibSDL::Scancode
def initialize(@key, @scan_code)
end
end
class KeyDown
getter key : LibSDL::Keycode
getter scan_code : LibSDL::Scancode
getter repeat : Bool
def initialize(@key, @scan_code, @repeat)
end
end
# FIXME: THIS MAIN LOOP IS **NOT** READY FOR PRODUCTION
# Update events are still missing, and both Draw and Update should
# be time-based.
# Why having a `window` parameter instead of making a Window#loop method?
# Because we may want to have a `windows` parameter in the future, although
# unlikely. But still possible.
def self.loop(window, &block)
max_fps = 60
max_dt = 1000. / 60
timer = Naka::Timer.new
renderer = window.renderer
::loop do
exit_requested = false
while event = SDL::Event.poll
r_value = yield case event
when SDL::Event::Quit
Naka::Event::Quit.new
when SDL::Event::Keyboard
keysym = event.keysym
if event.type == LibSDL::EventType::KEYUP
Naka::Event::KeyUp.new keysym.sym, keysym.scancode
elsif event.type == LibSDL::EventType::KEYDOWN
Naka::Event::KeyDown.new keysym.sym, keysym.scancode, event.repeat != 0
end
else # FIXME: Most event types may need proper Naka:: classes.
event
end
if r_value == Naka::Event::Quit
exit_requested = true
break
end
end
break if exit_requested
dt = timer.step
r_value = yield Naka::Event::Update.new dt
break if r_value == Naka::Event::Quit
renderer.draw_color = SDL::Color[255, 255, 255, 255]
renderer.clear
yield Naka::Event::Draw.new window
renderer.present
sleep 0.001
end
end
end

105
src/noise.cr Normal file
View File

@ -0,0 +1,105 @@
# FIXME: We may want to generate noise of other numbers of dimensions.
class Naka::Noise2D
class Octave
record Vec2, x : Float64, y : Float64
private def lerp(a, b, v)
a * (1.0 - v) + b * v
end
private def smooth(v)
v * v * (3.0 - 2.0 * v)
end
private def gradient(orig, grad, p)
sp = Vec2.new(p.x - orig.x, p.y - orig.y)
grad.x * sp.x + grad.y * sp.y
end
private def random_gradient
v = rand * Math::PI * 2.0
Vec2.new(Math.cos(v), Math.sin(v))
end
@frequency : Float64 | Int32
@amplitude : Float64 | Int32
def amplitude
@amplitude.to_f
end
def frequency
@frequency.to_f
end
def initialize(@frequency, @amplitude)
@rgradients = StaticArray(Vec2, 256).new { random_gradient }
@permutations = StaticArray(Int32, 256).new { |i| i }
@permutations.shuffle!
end
def get_gradient(x, y)
idx = @permutations[x & 255] + @permutations[y & 255]
@rgradients[idx & 255]
end
def get_gradients(x, y)
x0f = x.floor
y0f = y.floor
x0 = x0f.to_i
y0 = y0f.to_i
x1 = x0 + 1
y1 = y0 + 1
{
{
get_gradient(x0, y0),
get_gradient(x1, y0),
get_gradient(x0, y1),
get_gradient(x1, y1),
},
{
Vec2.new(x0f + 0.0, y0f + 0.0),
Vec2.new(x0f + 1.0, y0f + 0.0),
Vec2.new(x0f + 0.0, y0f + 1.0),
Vec2.new(x0f + 1.0, y0f + 1.0),
},
}
end
def get(x, y)
x = x.to_f / @frequency
y = y.to_f / @frequency
p = Vec2.new(x, y)
gradients, origins = get_gradients(x, y)
v0 = gradient(origins[0], gradients[0], p)
v1 = gradient(origins[1], gradients[1], p)
v2 = gradient(origins[2], gradients[2], p)
v3 = gradient(origins[3], gradients[3], p)
fx = smooth(x - origins[0].x)
vx0 = lerp(v0, v1, fx)
vx1 = lerp(v2, v3, fx)
fy = smooth(y - origins[0].y)
lerp(vx0, vx1, fy) * @amplitude.to_f
end
end
@storage = Array(Octave).new
def initialize
end
def initialize(a, f)
add_octave a, f
end
def add_octave(a, f)
@storage << Octave.new a, f
end
def get(x, y)
@storage.reduce 0 { |a, e| a + e.get(x, y) }
end
end