Classic Solitaire Klondike

There are no engines like Defold that have good mobile web.

By engines like Defold, I refer to engines that use emscripten for compilation. Take for example GameMaker and the limitations they have https://help.yoyogames.com/hc/en-us/articles/216754018-HTML5-Issues-And-Differences?mobile_site=true

They actually have html5 specific code in order to maximize performance (caching 4 sprites etc.). They tend to hyperoptimize code for each platform.

Unity gives you dialog saying mobile web is not supported with yes/no.

If anyone wants great HTML5 then only native has great performance so only engines using native JS like: Phaser or Cocos Creator (which has Spidermonkey and soon V8 engine for web and it uses cocos2dx bindings for native android/ios/desktop).

I use phaser for mobile html5 games. I’ll show you the same solitaire later on this week made using phaser.

Anyway, it’s not about “defold is bad” it’s about “defold is not enough good for mobile html5”.

UPD: (example removed, old link)
I really don’t know how many draw calls and other parameters in this game. I am sure that I can optimase it more, if you think that it works bad.
But I open this phaser solitaire and Defold solitaire , and phaser’s works better on my ipad4

3 Likes

Yes, the general advice is to use native builds on mobile. We’ll see what the future holds
for HTML5 builds. We don’t know what webassembly might bring performancewise, for instance.

3 Likes

Fully agree with you.

1 Like

For games in messengers, FB instant games, etc., main problem is HTML5 build size. Not only performance on mobile devices.

For example I had a discuss with Head of Games platform of big Russian social network and he said (and sad) that 6-7Mb of my game is too much for game in chat, he wants less or equal to 3Mb.

1 Like

yes, as I know
facebook.com instant games 5 Mb (recommendation, can be bigger)
ok.ru chat games 3 Mb (requirement, cannot be bigger)
vk.com direct games 10 Mb (requirement, cannot be bigger)

It’s again about my old topic

3 Likes

We made small update with final animation and some performance optimisations. Thank you @ross.grams (topic)

ios version still on review, but html5 and android released.

9 Likes

Awesome! :smiley:

By any chance are you planning on adding a sideways layout for mobile? My brother’s wife plays klondike on her iPhone all the time, but she had a version with ads, so I showed her yours. She missed the bouncing cards and being able to rotate her phone for the sideways layout though.

3 Likes

Yep, we plan to make landscape in the next version.

What do you mean with “bouncing”?

1 Like

Great!

I mean the part you just added, with the trails, when you win.

1 Like

At the end cards fall down one stack at a time, maybe make it all four altogether? Should look fancier.

When card is picked, make it bigger (scale up) and add a shadow underneath. That will make an illusion of a raised card above the table. The same technique can be used when a card is flipped, and since Defold is 3D, make a proper 3D rotation with perspective (when one side gets bigger during the rotation).

Add a texture background that looks more like a textile and some slight dark gradient to make a feel of a lamp nearby.

Cards transition linearly? Some easing could also make it look snappier.

No lose state determination?

3 Likes

Many of this things already in to-do list.
We have no plans for monetization of this game - we don’t want to spend many time for it. Time to time I will make some features and improvements, only when I have no any other things.
Thank you for your feedback and suggestions.

5 Likes

One more programmer joined to our team, which means that we will be able to do a little more =)
Our solitaire was updated to version 1.55
What’s new:

  • The game now can be played in landscape orientation (requested by @ross.grams =) )
  • Added button “Auto win” in case if all the cards are face up.
  • Reduced the size of the build.
  • Minor bugfixes.

Android and HTML5 version already updated. IOS build - Waiting for Review.

6 Likes

How much did you manage to reduce the size, and how?

The build size reduced using buildmanifest:

# Release + No: Physics + Record + Profiler + Facebook
platforms:

    js-web:
        context:
            excludeLibs: ["BulletDynamics", "BulletCollision", "LinearMath", "Box2D", "physics", "record", "vpx", "engine", "profilerext"]
            excludeSymbols: ["FacebookExt", "ProfilerExt"]
            libs: ["engine_release", "physics_null", "record_null"]

    armv7-android:
        context:
            excludeLibs: ["BulletDynamics", "BulletCollision", "LinearMath", "Box2D", "physics", "record", "vpx", "engine", "profilerext"]
            excludeSymbols: ["FacebookExt", "ProfilerExt"]
            libs: ["engine_release", "physics_null", "record_null"]

    armv7-ios:
        context:
            excludeLibs: ["BulletDynamics", "BulletCollision", "LinearMath", "Box2D", "physics", "record", "vpx", "engine", "profilerext"]
            excludeSymbols: ["FacebookExt", "ProfilerExt"]
            libs: ["engine_release", "physics_null", "record_null"]
            
    arm64-ios:
        context:
            excludeLibs: ["BulletDynamics", "BulletCollision", "LinearMath", "Box2D", "physics", "record", "vpx", "engine", "profilerext"]
            excludeSymbols: ["FacebookExt", "ProfilerExt"]
            libs: ["engine_release", "physics_null", "record_null"]

iOS:

Android:
OLD Version: 3.06 Mb
NEW Version: 2.58 Mb

Html5 gzip:
OLD Version :


NEW Version:

15 Likes

Wow, fantastic! :smiley:

1 Like

I just received few crash reports, maybe you can help, pls?

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Huawei/ALE-L23/hwALE-H:5.0.1/HuaweiALE-L23/C605B150:user/release-keys'
Revision: '0'
ABI: 'arm'
pid: 32342, tid: 32433, name: Thread-303  >>> com.potatojam.classic.solitaire.klondike <<<
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: '../src/opengl/graphics_opengl.cpp:1646: void dmGraphics::SetTexture(dmGraphics::HTexture, const dmGraphics::TextureParams&): assertion "params.m_Height <= g_Context->m_MaxTextureSize" failed'
    r0 00000000  r1 00007eb1  r2 00000006  r3 00000000
    r4 e302bdd8  r5 00000006  r6 00000002  r7 0000010c
    r8 e302b764  r9 e302b740  sl e3291adc  fp ab545438
    ip 00007eb1  sp e302b5e8  lr f707d699  pc f70a3a94  cpsr 600f0010
backtrace:
    #00 pc 0003ca94  /system/lib/libc.so (tgkill+12)
    #01 pc 00016695  /system/lib/libc.so (pthread_kill+52)
    #02 pc 000172a7  /system/lib/libc.so (raise+10)
    #03 pc 00013be5  /system/lib/libc.so (__libc_android_abort+36)
    #04 pc 000123bc  /system/lib/libc.so (abort+4)
    #05 pc 00014dc3  /system/lib/libc.so (__libc_fatal+16)
    #06 pc 00013c69  /system/lib/libc.so (__assert2+20)
    #07 pc 000d795c  /data/app/com.potatojam.classic.solitaire.klondike-1/lib/arm/libSolitaire_Klondike.so (dmGraphics::SetTexture(dmGraphics::Texture*, dmGraphics::TextureParams const&)+496)
    #08 pc 000d900c  /data/app/com.potatojam.classic.solitaire.klondike-1/lib/arm/libSolitaire_Klondike.so (dmGraphics::NewRenderTarget(dmGraphics::Context*, unsigned int, dmGraphics::TextureCreationParams const*, dmGraphics::TextureParams const*)+612)
    #09 pc 000e683c  /data/app/com.potatojam.classic.solitaire.klondike-1/lib/arm/libSolitaire_Klondike.so (dmRender::RenderScript_RenderTarget(lua_State*)+1512)
    #10 pc 0010f4f8  /data/app/com.potatojam.classic.solitaire.klondike-1/lib/arm/libSolitaire_Klondike.so
Tombstone written to: /data/tombstones/tombstone_00


logcat.txt.zip (148.1 KB)

And second:

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'samsung/j1acevelteub/j1acevelte:5.1.1/LMY47V/J111MUBU0AQF2:user/release-keys'
Revision: '4'
ABI: 'arm'
pid: 21771, tid: 21853, name: Thread-1856  >>> com.potatojam.classic.solitaire.klondike <<<
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: '../src/opengl/graphics_opengl.cpp:1646: void dmGraphics::SetTexture(dmGraphics::HTexture, const dmGraphics::TextureParams&): assertion "params.m_Height <= g_Context->m_MaxTextureSize" failed'
    r0 00000000  r1 0000555d  r2 00000006  r3 00000000
    r4 a2dcfdb8  r5 00000006  r6 00000009  r7 0000010c
    r8 a2dcf744  r9 a2dcf720  sl a302eadc  fp b4772e80
    ip 0000555d  sp a2dcf5c8  lr b6de3735  pc b6e06dbc  cpsr 600f0010
backtrace:
    #00 pc 0003adbc  /system/lib/libc.so (tgkill+12)
    #01 pc 00017731  /system/lib/libc.so (pthread_kill+52)
    #02 pc 00018347  /system/lib/libc.so (raise+10)
    #03 pc 00014be1  /system/lib/libc.so (__libc_android_abort+36)
    #04 pc 00012f70  /system/lib/libc.so (abort+4)
    #05 pc 00015e25  /system/lib/libc.so (__libc_fatal+16)
    #06 pc 00014c65  /system/lib/libc.so (__assert2+20)
    #07 pc 000d795c  /data/app/com.potatojam.classic.solitaire.klondike-1/lib/arm/libSolitaire_Klondike.so (_ZN10dmGraphics10SetTextureEPNS_7TextureERKNS_13TextureParamsE+496)
    #08 pc 000d900c  /data/app/com.potatojam.classic.solitaire.klondike-1/lib/arm/libSolitaire_Klondike.so (_ZN10dmGraphics15NewRenderTargetEPNS_7ContextEjPKNS_21TextureCreationParamsEPKNS_13TextureParamsE+612)
    #09 pc 000e683c  /data/app/com.potatojam.classic.solitaire.klondike-1/lib/arm/libSolitaire_Klondike.so (_ZN8dmRender25RenderScript_RenderTargetEP9lua_State+1512)
    #10 pc 0010f4f8  /data/app/com.potatojam.classic.solitaire.klondike-1/lib/arm/libSolitaire_Klondike.so
Tombstone written to: /data/tombstones/tombstone_01
[ 11-26 01:49:01.121   183:  183 E/         ]
ro.product_ship = true
[ 11-26 01:49:01.121   183:  183 E/         ]
ro.debug_level = 0x4f4c


logcat-2.txt.zip (290.0 KB)

Strange, but some error with m_MaxTextureSize
We have only one texture in game:
1024x256
and second one I make dynamically in render script:

local helper = require "modules.render.helper"
local msgs = require "modules.msgs"

local RT_halfsize = 50

local function update_render_targets(self, initial)
  if not initial then
    render.delete_render_target(self.render_target)
  end
  self.RT_color_params.width, self.RT_depth_params.width = helper.win_width, helper.win_width
  self.RT_color_params.height, self.RT_depth_params.height = helper.win_height, helper.win_height

  self.render_target = render.render_target("target", {[render.BUFFER_COLOR_BIT] = self.RT_color_params,
  [render.BUFFER_DEPTH_BIT] = self.RT_depth_params})

  local x = RT_halfsize * 2 / helper.win_width
  local y = RT_halfsize * 2 / helper.win_height
  self.RT_proj = vmath.matrix4_orthographic(-RT_halfsize + x, RT_halfsize - x,
  - RT_halfsize + y, RT_halfsize - y, - 1, 1)

  self.firstFrame = true
end

local function update_window(self)
  helper.win_width = render.get_window_width()
  helper.win_height = render.get_window_height()

  if helper.win_width >= helper.win_height then
    helper.settings_width = render.get_height()
    helper.settings_height = render.get_width()
  else
    helper.settings_width = render.get_width()
    helper.settings_height = render.get_height()
  end

  helper.zoom_factor = math.min(helper.win_width / helper.settings_width, helper.win_height / helper.settings_height)
  self.projected_width = helper.win_width / helper.zoom_factor
  self.projected_height = helper.win_height / helper.zoom_factor
  helper.xoffset = - (self.projected_width - helper.settings_width) / 2
  helper.yoffset = - (self.projected_height - helper.settings_height) / 2

  helper.adjust_stretch_x = helper.win_width / helper.settings_width
  helper.adjust_stretch_y = helper.win_height / helper.settings_height
  helper.adjust_zoom = math.max(helper.adjust_stretch_x, helper.adjust_stretch_y)
  helper.adjust_fit = math.min(helper.adjust_stretch_x, helper.adjust_stretch_y)
  self.full_proj = vmath.matrix4_orthographic(helper.xoffset, helper.xoffset + self.projected_width,
  helper.yoffset, helper.yoffset + self.projected_height, - 1, 1)

  self.view = vmath.matrix4()
  self.gui_projection = vmath.matrix4_orthographic(0, helper.win_width, 0, helper.win_height, - 1, 1)
end

function init(self)
  self.tile_pred = render.predicate({"tile"})
  self.gui_pred = render.predicate({"gui"})
  self.text_pred = render.predicate({"text"})
  self.render_target_pred = render.predicate({"render_target"})

  helper.settings_width = render.get_width()
  helper.settings_height = render.get_height()

  self.clear_color = vmath.vector4(0, 0, 0, 0)
  self.clear_color.x = sys.get_config("render.clear_color_red", 0)
  self.clear_color.y = sys.get_config("render.clear_color_green", 0)
  self.clear_color.z = sys.get_config("render.clear_color_blue", 0)
  self.clear_color.w = sys.get_config("render.clear_color_alpha", 0)

  self.RT_color_params = {
    format = render.FORMAT_RGBA,
    width = render.get_window_width(),
    height = render.get_window_height(),
    min_filter = render.FILTER_LINEAR,
    mag_filter = render.FILTER_LINEAR,
    u_wrap = render.WRAP_CLAMP_TO_EDGE,
    v_wrap = render.WRAP_CLAMP_TO_EDGE
  }
  self.RT_depth_params = {
    format = render.FORMAT_DEPTH,
    width = render.get_window_width(),
    height = render.get_window_height(),
    u_wrap = render.WRAP_CLAMP_TO_EDGE,
    v_wrap = render.WRAP_CLAMP_TO_EDGE
  }

  self.clear_param = {
    [render.BUFFER_COLOR_BIT] = self.clear_color,
    [render.BUFFER_DEPTH_BIT] = 1,
    [render.BUFFER_STENCIL_BIT] = 0
  }

  self.view = vmath.matrix4()
  update_window(self)
  update_render_targets(self, true)
end

function update(self)
  render.set_depth_mask(true)
  render.clear(self.clear_param)

  render.set_viewport(0, 0, helper.win_width, helper.win_height)
  render.set_view(self.view)
  render.set_depth_mask(false)
  render.disable_state(render.STATE_DEPTH_TEST)
  render.disable_state(render.STATE_STENCIL_TEST)
  render.enable_state(render.STATE_BLEND)
  render.set_blend_func(render.BLEND_SRC_ALPHA, render.BLEND_ONE_MINUS_SRC_ALPHA)
  render.disable_state(render.STATE_CULL_FACE)

  render.set_projection(self.full_proj)

  if self.is_render_target then

    render.enable_render_target(self.render_target)
    if self.firstFrame then
      self.firstFrame = false
      render.clear(self.clear_param)
    end

    render.draw(self.tile_pred)
    render.disable_render_target(self.render_target)

    render.set_view(self.view)
    render.set_projection(self.RT_proj)
    render.enable_texture(0, self.render_target, render.BUFFER_COLOR_BIT)
    render.draw(self.render_target_pred)
    render.disable_texture(0, self.render_target)
  else
    render.draw(self.tile_pred)
  end

  render.set_view(self.view)
  render.set_projection(self.gui_projection)

  render.enable_state(render.STATE_STENCIL_TEST)
  render.draw(self.gui_pred)
  render.draw(self.text_pred)
  render.disable_state(render.STATE_STENCIL_TEST)

  render.set_depth_mask(false)
end

function on_message(self, message_id, message)
  if message_id == hash("clear_color") then
    self.clear_color = message.color
    self.clear_param[render.BUFFER_COLOR_BIT] = self.clear_color
  elseif message_id == hash("set_view_projection") then
    self.view = message.view
  elseif message_id == msgs.WINDOW_RESIZED then
    update_window(self)
    update_render_targets(self)
    msg.post("main:/main#main", message_id, message)
  elseif message_id == msgs.R_TARGET then
    self.is_render_target = message.isOn
    if self.is_render_target then
      update_render_targets(self)
    end
  end
end

The assert you are seeing is a check so that we don’t create a render target with dimensions greater than what is supported by the device

glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size);
...
assert(params.m_Height <= max_texture_size);

Maybe helper.win_width for some reason get assigned too big a value?

1 Like

I have only one place where I set this value (in render script):

 helper.win_width = render.get_window_width()
  helper.win_height = render.get_window_height()

Both device has smaller screen size than supported texture.
Is it possible that Init of render_sript runs earliar that initialization of global render module and I recieve some default value in render.get_window_width() instead of real size?
(I have no this device to check this value - logs from google console)

1 Like

There are also some glGetError checks when creating textures in dmGraphics::SetTexture which could unfortunately result in assert if they fail. However I can’t see any issues with the parameters you pass it…

But it might be a good idea to do these to things (based on my own experience) anyway:

  • Make sure your render targets are power-of-two in size (even if CLAMP_TO_EDGE should be able to handle NPOT textures).
  • Switch to NEAREST filtering unless you really need LINEAR (I guess you use the render target as a post-process so it’s always 1:1 from texture to screen fragments, so linear doesn’t really do much here anyway).
4 Likes