Jump to content

Josh

Staff
  • Posts

    23,091
  • Joined

  • Last visited

Blog Entries posted by Josh

  1. Josh
    One of the downsides of deferred rendering is it isn't very good at handling transparent surfaces. Since we have moved to a new forward renderer, one of my goals in Leadwerks 5 is to have easy hassle-free transparency with lighting and refraction that just works.
    Pre-multiplied alpha provides a better blending equation than traditional alpha blending. I'm not going to go into the details here, but it makes it so the transparent surface can be brighter than the underlying surface, as you can see on the vehicle's windshield here:

    I've been working for a while to build an automatic post-processing step into the engine that occurs when a transparency object is onscreen. If no transparent objects are onscreen, then the post-processing step can be skipped.
    You can also call Camera::SetRefraction(false) and just use regular GPU-blended transparency with no fancy refraction of the background, but I plan to enable it by default.
    To use this effect, there is absolutely nothing you have to do except to create a material, make it transparent, and apply it to a mesh somewhere.
    auto mtl = CreateMaterial(); mtl->SetTransparent(true); mtl->SetColor(1,1,1,0.5); The lower the alpha value of the material color, the more see-through it is. You can use an alpha value of zero to make a refractive predator-like effect.
     
  2. Josh
    A new update is available that adds post-processing effects in Leadwerks 5 beta.

    To use a post-processing effect, you load it from a JSON file and apply it to a camera like so:
    auto fx = LoadPostEffect("Shaders/PostEffects/SSAO.json"); camera->AddPostEffect(fx); You can add as many effects as you want, and they will be executed in sequence.
    The JSON structure looks like this for a simple effect:
    { "postEffect": { "subpasses": [ { "shader": { "vertex": "Shaders/PostEffects/PostEffect.vert.spv", "fragment": "Shaders/PostEffects/SSAO.frag.spv" } } ] } } Multiple subpasses are supported for custom blurring and chains of shaders. This Gaussian blur effect uses several intermediate buffers to blur and downsample the image:
    { "postEffect": { "buffers": [ { "size": [0.5, 0.5] }, { "size": [0.25, 0.25] }, { "size": [0.125, 0.125] } ], "subpasses": [ { "target": 0, "shader": { "vertex": "Shaders/PostEffects/PostEffect.vert.spv", "fragment": "Shaders/PostEffects/blurx.frag.spv" } }, { "target": 1, "shader": { "vertex": "Shaders/PostEffects/PostEffect.vert.spv", "fragment": "Shaders/PostEffects/blury.frag.spv" } }, { "target": 2, "shader": { "vertex": "Shaders/PostEffects/PostEffect.vert.spv", "fragment": "Shaders/PostEffects/blurx.frag.spv" } }, { "shader": { "vertex": "Shaders/PostEffects/PostEffect.vert.spv", "fragment": "Shaders/PostEffects/blury.frag.spv" } } ] } } A new file is located in "Config/settings.json". This file contains information for the engine when it initializes. You can specify a default set of post-processing effects that will automatically be loaded whenever a camera is created. If you don't want any post-processing effects you can either change this file, or call Camera::ClearPostEffects() after creating a camera.
    Customizable properties are not yet supported but I plan to add these so you can modify the look of an effect on-the-fly.
    Fixed physics bug reported by @wadaltmon
    Other changes:
    EnablePhysics is renamed to SetPhysicsMode. EnableGravity is renamed to SetGravityMode. EnableSweptCollision is renamed to SetSweptCollision. COLLISION_CHARACTER is renamed to COLLISION_PLAYER
  3. Josh
    The polygon voxelization process for our voxel GI system now takes vertex, material, and base texture colors into account. The voxel algorithm does not yet support a second color channel for emission, but I am building the whole system with that in mind. When I visualize the results of the voxel building the images are pretty remarkable! Of course the goal is to use this data for fast global illumination calculations but maybe they could be used to make a whole new style of game graphics.

    Direct lighting calculations on the CPU are fast enough that I am going to stick with this approach until I have to use the GPU. If several cascading voxel grids were created around the camera, and each updated asynchronously on its own thread, that might give us the speed we need to relieve the GPU from doing any extra work. The final volume textures could be compressed to DXT1 (12.5% their original size) and sent to the GPU.
    After direct lighting has been calculated, the next step is to downsample the voxel grid. I found the fastest way to do this is to iterate through just the solid voxels. This is how my previous algorithm worked:
    for (x=0; x < size / 2; ++x) { for (y=0; y < size / 2; ++y) { for (z=0; z < size / 2; ++z) { //Downsample this 2x2 block } } } A new faster approach works by "downsampling" the set of solid voxels by dividing each value by two. There are some duplicated values but that's fine:
    for (const iVec3& i : solidvoxels) { downsampledgrid->solidvoxels.insert(iVec3(i.x/2,i.y/2,i.z/2)) } for (const iVec3& i : downsampledgrid->solidvoxels) { //Downsample this 2x2 block } We can then iterate through just the solid voxels when performing the downsampling. A single call to memset will set all the voxel data to black / empty before the downsampling begins. This turns out to be much much faster than iterating through every voxel on all three axes.
    Here are the results of the downsampling process. What you don't see here is the alpha value of each voxel. The goblin in the center ends up bleeding out to fill very large voxels, because the rest of the volume around him is empty space, but the alpha value of those voxels will be adjusted to give them less influence in the GI calculation.




    For a 128x128x128 voxel grid, with voxel size of 0.125 meters, my numbers are now:
    Voxelization: 607 milliseconds Direct lighting (all six directions): 109 First downsample (to 64x64): 39 Second downsample (to 32x32): 7 Third downsample (to 16x16): 1 Total: 763 Note that voxelization, by far the slowest step here, does not have to be performed completely on all geometry each update. The direct lighting time elapsed is within a tolerable range, so we are in the running to make GI calculations entirely on the CPU, relieving the GPU of extra work and compressing our data before it is sent over the PCI bridge.
    Also note that a smaller voxel grids could be used, with more voxel grids spread across more CPU cores. If that were the case I would expect our processing time for each one to go down to 191 milliseconds total (39 milliseconds without the voxelization step), and the distance your GI covers would then be determined by your number of CPU cores.
    In fact there is a variety of ways this task could be divided between several CPU cores.
  4. Josh
    A new beta is uploaded with lots of new features and improvements. Things are really taking shape!
    Animation is now supported and it's really fast. Two examples are included. Package loader plugins now supported, VPK package loader for Source Engine games included with example. Added localization example. Shaders folder very neatly organized, now contains shader family files. Config folder eliminated. Engine headers cleaned up and organized. Lots of third party libraries removed. SVG texture loader removed. Printed console output is now much quieter. Current directory is stripped from load messages. DebugError() command renamed to RuntimeError(). DebugWarning() command renamed to Warning().
  5. Josh
    Now that we have lights working in our clustered forward Vulkan renderer (same great technique the latest DOOM games are using) I am starting to implement shadow maps. The first issue that came up was managing render-to-texture when the texture might still be in use rendering the previous frame. At first I thought multiple shadowmaps would be needed per light, like a double-buffering system, but that would double the number of shadow textures and video memory. Instead, I created a simple object pool which stores spare shadowmaps that can be swapped around and used as they are needed.
    It turns out I already have pretty much all the code I need because Vulkan's swapchain creation works exactly the same way, by rendering to a series of images. I worked through the code slowly and came up with this by the end of the day, which runs successfully without throwing any validation errors:
    bool RenderBuffer::Initialize(shared_ptr<RenderContext> context, const int width, const int height, const int depth, const int colorcomponents, const bool depthcomponent, const int samples) { this->device = GameEngine::Get()->renderingthreadmanager->instance; int totalcomponents = colorcomponents; if (depthcomponent) totalcomponents++; std::fill(colortexture.begin(), colortexture.end(), nullptr); depthtexture = nullptr; //Create color images for (int i = 0; i < colorcomponents; ++i) { colortexture[i] = make_shared<RenderTexture>(); colortexture[i]->Initialize(VK_IMAGE_TYPE_2D, device->chaininfo.imageFormat, width, height, 1, 0, false, -1,-1,-1,-1,1, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT); colorimages[i] = colortexture[i]->vkimage; } //Create depth image if (depthcomponent) { depthtexture = make_shared<RenderTexture>(); depthtexture->Initialize(VK_IMAGE_TYPE_2D, device->depthformat, width, height, 1, samples, false, -1, -1, -1, -1, 1, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); } //Create image views imageviews.resize(totalcomponents); VkImageViewCreateInfo createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; createInfo.subresourceRange.baseMipLevel = 0; createInfo.subresourceRange.levelCount = 1; createInfo.subresourceRange.baseArrayLayer = 0; createInfo.subresourceRange.layerCount = 1; // Create color image views for (size_t i = 0; i < colorcomponents; i++) { createInfo.image = colorimages[i]; createInfo.format = device->chaininfo.imageFormat; createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; VkAssert(vkCreateImageView(device->device, &createInfo, nullptr, &imageviews[i])); } //Create depth image view if (depthcomponent) { createInfo.image = depthtexture->vkimage; createInfo.format = depthtexture->format; createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; VkAssert(vkCreateImageView(device->device, &createInfo, nullptr, &imageviews[colorcomponents])); } //Create framebuffer VkFramebufferCreateInfo framebufferInfo = {}; framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; framebufferInfo.renderPass = device->renderpass->pass; framebufferInfo.attachmentCount = 2; framebufferInfo.pAttachments = imageviews.data(); framebufferInfo.width = width; framebufferInfo.height = height; framebufferInfo.layers = 1; VkAssert(vkCreateFramebuffer(device->device, &framebufferInfo, nullptr, &framebuffer)); return true; } The blog cover image isn't the Vulkan renderer, I just found a random image of Leadwerks shadows.
  6. Josh
    Following this tutorial, I have managed to add uniform buffers into my Vulkan graphics pipeline. Since each image in the swapchain has a different graphics pipeline object, and uniform buffers are tied to a pipeline, you end up uploading all the data three times every time it changes. OpenGL might be doing something like this under the hood, but I am not sure this is a good approach. There are three ways to get data to a shader in Vulkan. Push constants are synonymous with GLSL uniforms, although much more restrictive. Uniform buffers are the same in OpenGL, and this is where I store light data in the clustered forward renderer. Shader storage buffers are the slowest to update but they can have a very large capacity, usually as big as your entire VRAM. I have the first two working now. Below you can see a range of instanced boxes being rendered with an offset for each instance, which is being read from a uniform buffer using the instance ID as the array index.

    To prove that Vulkan can render more than just boxes, here is a model loaded from GLTF format and rendered in Vulkan:

    Figuring out the design of the new renderer using OpenGL was very smart. I would not have been able to invent it if I had jumped straight into Vulkan.
  7. Josh
    I'm building the VR project template for Leadwerks 4.5.  Although you can enable VR in any project, this template is specifically designed to provide some of your most common room-scale VR features:
    Teleportation movement, which prevents motion sickness. Picking up and throwing objects. (It's actually really fun!) To start with I am creating the art assets for the teleport effect. This is basically what I want:

    Your controller shoots a beam which ends in an indicator when it hits an upwards-facing slope. Typically this beam will be somewhat arced.  Why the curve? This allows you to climb up to areas above you:

    As always, I am starting with the game assets. I don't believe in using programmer art because it hurts your understanding of what you are trying to create, it's uninspiring, and you will end up writing your code twice once you get the final artwork and realize all the mistakes you made.
    I started with textures. I know I want a circular indicator on the floor, a misty spinning effect rising off it, and a beam. I'm going to make all my textures grayscale so that I can control the color with the entity color value and dynamically change it in the game.  Here are my textures I created in about ten minutes in Paint Shop Pro:



    The first texture above is clamped along the X and Y axes and the second one is clamp along the Y axis.  I am using uncompressed textures for all of these because they have a lot of soft gradients.
    I created my materials with the following settings, again leaving everything white:

    In 3ds Max I created my indicator model. It's just a plane with a cylinder on top, with the end caps removed:

    When I import it into Leadwerks and apply my materials, the model looks like this:

    I'll show you why I am using uncompressed textures. You can see in this shot the edge of the ring has some ugly artifacts when texture compression is used:

    Here's a closeup. Not something I want to see in VR:

    Now I am going to create an instance of the model in the editor and adjust the color. I want a bright blue glowy color. I am setting the color to RGB 128,255,255 and cranking the intensity way up to 2.0. This effectively sets the entity color to 256,512,512. This color is multiplied by the texture color at each pixel and then clamped to 0-255 (the maximum color range of the monitor). That means that the brightest spots on the material will reach a full 255,255,255 white color and look really intense, while darker parts will be tinted blue:

    Notice the object isn't just a flat color, but has a range of color from blue to white. To get this effect I had to increase the intensity over 1.0 to create colors brighter than RGB 255,255,255, and I had to have some red in the color. If I had set the color to RGB 0,255,255 the red channel would never increase and I would have a flat color like this. Not so good:

    If I had set the color to RGB 128,255,255 but left the intensity at 1.0 I would also have a solid color:

    Finally I added a script to the model and saved it as a prefab. The script just rotates the model around slowly on its Y axis, which I think will look pretty good. I'm going to perform the rotation in the Draw() function so it doesn't get called if the object is hidden or offscreen, and I don't think anyone will notice if the rotation doesn't update when they look away:
    function Script:Draw() self.entity:Turn(0, 0.1 * Time:GetSpeed(), 0) end That's it for now. The next step will be to create my teleportation mechanic in VR.
  8. Josh
    I've updated the editor and Lua executables on Windows with the following fixes:
    http://www.leadwerks.com/werkspace/topic/15399-rc4-editor-hints-get-built-in-probe-reflections/
    http://www.leadwerks.com/werkspace/topic/15646-prefabs-child-entity-scripts-start-function-not-called/
    http://www.leadwerks.com/werkspace/topic/15644-latest-43-beta-causes-crawler-to-rotate-at-wrong-direction/
    http://www.leadwerks.com/werkspace/topic/15593-releasecleanup-vehicle-error/
     
    The game launcher beta branch for Windows has also been updated. If all goes well, 4.3 will be released next week.
  9. Josh
    A small update is out, on all branches, that fixes the vehicle wheels not being positioned correctly:
    http://www.leadwerks.com/werkspace/topic/15405-problembug-with-vehicles-cars-wheels-dont-move-since-update-4-2
  10. Josh
    Keeping your social media accounts active with screenshots, videos, and updates is important, but as an indie developer you probably don't have time to post on there every day. In this blog I will show you how to easily automate your social media accounts so that a constant stream of new content is going out to your fans.
     
    First, you will need to create a free account with dlvr.it. Connect your social media accounts to it. Facebook, Twitter, and Google+ are the important ones.
     
    Now you need some RSS feeds to act as inputs. There is an RSS feed for your game's announcements here:
    http://steamcommunity.com/games/YOUR_APP_ID/rss/
     
    Set this RSS feed as an input in your dlvr.it account and it will make posts automatically to your social media account.
     
    We can use the Steam web API to create an RSS feed of your game's screenshots, videos, and Workshop items. The code below will do exactly that. Just input your game's app ID and a web API key you generate in the Steamworks interface:

    /*----------------------------------------------------*/ /* This code is free to use. Do not redistribute it. */
    /* Leadwerks Software 2016 */
    /*----------------------------------------------------*/
     
    $appID = 'xxxxxxx';
    $webAPIKey = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
    $recordCount=20;
    $fileType = '-1';
    $queryType = 1;
     
    function LoadXML($path)
    {
    $content = utf8_encode(file_get_contents($path));
    $content = str_replace(chr(11),'',$content);
    if (trim($content=='')) return NULL;
    return simplexml_load_string($content);
    }
     
    $steamcontent = @LoadXML('http://api.steampowered.com/IPublishedFileService/QueryFiles/v0001?key='.$webAPIKey.'&format=xml&query_type='.$queryType.'&page=1&numperpage='.$recordCount.'&appid='.$appID.'&filetype='.$fileType.'&return_vote_data=1&return_short_description=1');
     
    function clean($string)
    {
    return preg_replace('/[^A-Za-z0-9\- ().,!]/', '', $string); // Removes special chars.
    }
     
    function truncate($string, $length)
    {
    if (strlen($string)>$length)
    {
    return substr($string,0,$length-3)."...";
    }
    else
    {
    return substr($string,0,$length);
    }
    }
     
    echo('<?xml version="1.0" encoding="UTF-8" ?>'."\n");
    echo('<rss version="2.0">'."\n");
    echo('<channel>'."\n");
    echo("<title>Leadwerks Community Activity</title>"."\n");
    echo('<link>http://www.leadwerks.com</link>'."\n");
    echo("<description>Activity feed from Leadwerks Community Hub on Steam.</description>"."\n");
     
    for ($i=0; $i<min($recordCount,$steamcontent->total); $i=$i+1)
    {
    /* Only show screenshots above a certain score */
    if (($steamcontent->publishedfiledetails->message[$i]->vote_data->score>0.4 || $steamcontent->publishedfiledetails->message[$i]->file_type!=5) && $steamcontent->publishedfiledetails->message[$i]->result==1)
    {
    $file_type = $steamcontent->publishedfiledetails->message[$i]->file_type;
    if ($file_type!=0 && $file_type!=5 && $file_type!=4) continue;
    echo("<item>"."\n");
     
    $title = clean($steamcontent->publishedfiledetails->message[$i]->title);
    if ($title=="") $title = truncate(clean($steamcontent->publishedfiledetails->message[$i]->short_description),35);
    if ($title=="Screenshot") $title = "";
     
    /* Get author name */
    $userid = $steamcontent->publishedfiledetails->message[$i]->creator;
    $userdata = simplexml_load_file('http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?format=xml&key='.$webAPIKey.'&steamids='.$userid);
    $username = $userdata->players->player[0]->personaname;
     
    if ($steamcontent->publishedfiledetails->message[$i]->file_type==0)
    {
    echo("\t<title>Workshop item by ".$username.": ".$title."</title>\n");
    }
    else if ($steamcontent->publishedfiledetails->message[$i]->file_type==5)
    {
    echo("\t<title>Screenshot by ".$username.": ".$title."</title>\n");
    }
    else if ($steamcontent->publishedfiledetails->message[$i]->file_type==4)
    {
    echo("\t<title>Video by ".$username.": ".$title."</title>\n");
    }
    else
    {
    continue;
    }
     
    if ($steamcontent->publishedfiledetails->message[$i]->file_type==4)
    {
    echo("\t<link>http://www.youtube.com/watch?v=".$steamcontent->publishedfiledetails->message[$i]->youtubevideoid."</link>\n");
    }
    else
    {
    echo("\t<link>http://steamcommunity.com/sharedfiles/filedetails/?id=".$steamcontent->publishedfiledetails->message[$i]->publishedfileid."</link>\n");
    }
     
    echo("\t<description>\n\t\t<![CDATA[");
     
    $description = clean($steamcontent->publishedfiledetails->message[$i]->short_description);
    $description = $description.' #gamedev #indiedev';
     
    if ($steamcontent->publishedfiledetails->message[$i]->file_type==4)
    {
    if (!empty($description)) echo('<p />'.$description);
    echo("<br />http://www.youtube.com/watch?v=".$steamcontent->publishedfiledetails->message[$i]->youtubevideoid);
    }
    else
    {
    echo("<p /><a href='http://steamcommunity.com/sharedfiles/filedetails/?id=".$steamcontent->publishedfiledetails->message[$i]->publishedfileid."'><img width='600px;' src='".$steamcontent->publishedfiledetails->message[$i]->preview_url."' /></a>");
    if (!empty($description)) echo('<br />'.$description);
    }
     
    echo("]]>\n\t</description>\n");
     
    if ($steamcontent->publishedfiledetails->message[$i]->file_type!=4)
    {
    echo("<enclosure url='".$steamcontent->publishedfiledetails->message[$i]->preview_url."' length='".$steamcontent->publishedfiledetails->message[$i]->file_size."' type='image/jpeg' />");
    }
     
    echo("<guid>".$steamcontent->publishedfiledetails->message[$i]->publishedfileid."</guid>");
    $displaytime = $steamcontent->publishedfiledetails->message[$i]->time_created;
    $displaytime = date('D, d M Y, h:m:s', intval($displaytime));
     
    echo("\t<pubDate>".$displaytime."</pubDate>\n");
     
    echo("\t<category>gamedev</category>\n");
    echo("\t<category>indiedev</category>\n");
    echo("\t<category>Leadwerks</category>\n");
     
    echo("</item>\n");
    }
    }
     
    echo("</channel>\n");
    echo("</rss>\n");
     
    There are a lot of settings in dlvr.it you can experiment with, but this is enough to get you running. Once this is set up you can keep your social media accounts active without having to log in and post items manually several times a day.
  11. Josh
    The Workshop Store interface has been updated. These changes will go out to the in-editor store interface soon.
     

     
    Clicking on the "Buy" button now opens the item directly in the Steam client, so you no longer have to log into the Steam website. No credit card is needed if you already have one on file in your Steam account.
     

  12. Josh
    The beta branch now contains an update that adds C++11 support for GCC on Linux. To use this you must enable C++11 support in the compiler settings in Code::Blocks. Select the Settings > Compiler and Debugger... menu item and then check the box indicated below.
     

     
    All new projects created from the Leadwerks templates will work correctly out-of-the-box.
     
    Your existing projects need a couple of new libraries added to them. The easiest way to do this is to open the CBP project file in a text editor. Find two chunks of text that look like this:

    <Linker> <Add library="$(LeadwerksPath)/Library/Linux/Debug/Leadwerks.a" /> <Add library="dl" /> <Add library="openal" /> <Add library="GL" /> <Add library="GLU" /> <Add library="$(LeadwerksPath)/Library/Linux/libluajit.a" /> <Add library="../../libsteam_api.so" /> <Add library="X11" /> <Add library="Xext" /> <Add library="pthread" /> </Linker>
     
    Add these two libraries to the list. Remember, this will occur twice in the file.

    <Add library="Xrender" /> <Add library="Xft" />
     
    Save the project file and you're ready to use C++11 features with Leadwerks on Linux.
  13. Josh
    Most of the prizes are now shipped, and I am just cleaning up a few pieces of missing information for shipping. In order to ship posters, I need a full name, which I have requested from onaid, MDGunn, Evayr, and Graham. I also am missing a shirt size for Garlic Waffle and MDGunn. I have not received any shipping info from MartyJ, or else I missed it.
     
    I found some mailing tubes from ULine to send posters in, at about $0.75 each. If you are receiving a sticker it will be included in the tube. Other prizes are sent in a separate envelope or package. USPS offered the best shipping rate, with the most expensive ones being about $13 compared to $100 each for UPS shipping.
     

     
    Whew! Shipping has been a pretty big ordeal this time with 20 entrants to handle, plus posters for each entrant. In order to make the next tournament easier, I have added a new information section to your Leadwerks account. You can now add your full name, shipping address, and shirt size by editing your profile. This information will remain private on your account, visible only by yourself and me. Next time around we are going to use this instead of trying to message back and forth with each participant, and I think it will make life simpler for everyone.
     
    The posters were a big hit and I think they were the main reason there were so many participants this time. The idea of working on a one-time event and getting some memorabilia for your effort is a really cool concept and gives the tournaments a new sense of liveliness. I think we will keep doing that.
     

     
    The
    of the resulting games was fun to make, and I think people liked watching it, but it only got 800 views. Without some additional method of promoting this content I don't think it's worth producing. I think a way to spread that kind of content needs to be found before more of it can be made, so that it can be used to promote your games. 
    The timing of the summer tournament was kind of bad because it started too late in the summer. In the future we should kick this off along with the Steam summer sale. There was only a 30 day window between the end of the summer tournament and the beginning of Halloween. In between that, the shipping issues and Steam Dev Days we were not able to hold a Halloween tournament, although you can participate in the community Script Challenge. I love seeing
    and the winter games tournament is tentatively named "Dead of Winter". I'm going to find a new poster artist and get the artwork started soon. 
    Leadwerks Game Launcher now has 90 games on Steam Workshop! The next tournament will definitely push us over the edge to 100 games, a goal that seemed very far away when we released the game launcher. Having 100 games on Steam will be a great achievement for the community, and a fantastic way to start 2017!
  14. Josh
    I've begun a Lua script for a Treeview widget. This is arguably one of the most complex widgets to implement, but after having done this in another language I have a design that I think is pretty simple. The script sorts all tree view nodes into a linear list-like table so that they can be drawn quickly. Like other widgets, the script will calculate where the starting point in the list is to display just the visible nodes. This is very important because it prevents the program from slowing down when the number of nodes gets really high and goes beyond the bounds of the scrollable area.
     

    Script.itemheight = 18 Script.itemindent = 20 function Script:Start() self.color = {} self.color.background = Vec4(0.2,0.2,0.2,1) self.color.foreground = Vec4(0.7,0.7,0.7,1) self.color.border = Vec4(0,0,0,1) end function Script:Draw() local gui = self.widget:GetGUI() local pos = self.widget:GetPosition(true) local sz = self.widget:GetSize(true) local scale = self.widget:GetGUI():GetScale() local style = self.widget:GetStyle() --Update table of visible nodes if self.visiblenodes==nil then self:UpdateNodes(self.widget) end --Draw background gui:SetColor(self.color.background.r,self.color.background.g,self.color.background.b,self.color.background.a) gui:DrawRect(pos.x,pos.y,sz.width,sz.height) --Draw nodes local n for n=1,#self.visiblenodes do gui:SetColor(self.color.foreground.r,self.color.foreground.g,self.color.foreground.b,self.color.foreground.a) gui:DrawText(self.visiblenodes[n].widget:GetText(),pos.x + 2*scale + self.visiblenodes[n].indent,pos.y + 2*scale + self.itemheight*(n-1),100,20) end --Draw border gui:SetColor(self.color.border.r,self.color.border.g,self.color.border.b,self.color.border.a) gui:DrawRect(pos.x,pos.y,sz.width,sz.height,1) end --Place nodes into a linear list function Script:UpdateNodes(node,indent) if indent==nil then indent=0 end if self.visiblenodes==nil then self.visiblenodes = {} end local n local count = node:CountChildren() local subnode local gui = self.widget:GetGUI() if node:Collapsed()==false then for n=0,count-1 do subnode = node:GetChild(n) self.visiblenodes[#self.visiblenodes+1] = {} self.visiblenodes[#self.visiblenodes].widget = subnode self.visiblenodes[#self.visiblenodes].indent = indent self:UpdateNodes(subnode,indent+self.itemindent) end end end
     
    The linear list also allows you to instantly calculate the node under a mouse coordinate, so you can figure out which node was clicked without iterating through hundreds of nodes. It's very similar to the design of the listview widget, but with the added complexity of a hierarchy that can be expanded and collapsed.
     

  15. Josh
    Over the recent months the Leadwerks Workshop Store has experienced a noticeable increase in sales, correlating to an increased number of products offered. Although Workshop Store sales are quite a bit lower than DLC sales, the Workshop Store is more scaleable because it allows third parties to submit a greater variety of products.
     
    We're also able to send out payments to Russia and other regions that other stores and websites may have trouble sending payments to, because our transaction system is built on Steam.
     

     
    I am meeting with Valve in October to make my recommendations on how we can maximize sales through this system for third-party authors. In the meantime, you can submit your items to the Workshop now:
    http://www.leadwerks.com/werkspace/page/tutorials/_/workshop-r11
  16. Josh
    I've got the Steamworks HTML renderer working with Leadwerks GUI now. The current build on Steam uses the Windows HTML renderer, which is part of IE. This isn't cross-platform, which is one reason why game launcher isn't available for Linux right now. The Chromium-based Steamworks HTML renderer is cross-platform but there are some issues.
     
    The HTML renderer works by rendering a web page into a memory buffer and then uploading that buffer to a texture and drawing it onscreen. The visible rendering is performed in a 3D graphics context, which means the Steam overlay will appear in it. So for example, it shows the little notification box in the lower-right popping up when you start the application. In Leadwerks Editor, we have the Steam overlay disabled for the entire application. This prevents the Steam overlay from appearing on all the viewports and 3D views in Leadwerks. This is also why games launched from Leadwerks Editor do not display the Steam overlay.
     
    In game launcher, we DO want the overlay to appear on launched games. So disabling it for that application isn't an option.
     
    At the same time, game launcher presently relies on an internet connection to a web page on our server that uses the Steam web API to query Steam's servers and display a grid of games. I've wanted to make it work in offline mode for a while, and now I have another reason to do this.
     
    So it looks like I will be writing a routine that builds a list of all available games, downloads their preview images, and creates an initial cache. The preview images will then be displayed in a custom widget I will write with a Leadwerks GUI widget script.
     
    Long story short, Game Launcher is going to ditch the HTML interface, work offline, and is coming to Linux. This blog entry was a window into some of the design decisions I deal with every day.
     

  17. Josh
    It took a few hours to add scrolling to the textfield widget. Now when the caret moves beyond the bounds of the box, text will slide over to keep the caret visible. This really isn't much different from a full text editor. The script is available now on the beta branch on Steam and presently weights in at 381 lines of Lua code.
     


     
    The Draw function is pretty short and easy to understand. I think most people will customize the appearance of these widgets more than the behavior:

    function Script:Draw(x,y,width,height) local gui = self.widget:GetGUI() local pos = self.widget:GetPosition(true) local sz = self.widget:GetSize(true) local scale = gui:GetScale() local item = self.widget:GetSelectedItem() local text = self.widget:GetText() self:UpdateOffset() --Draw the widget background gui:SetColor(0.2,0.2,0.2) gui:DrawRect(pos.x,pos.y,sz.width,sz.height,0) --Draw the widget outline if self.hovered==true then gui:SetColor(51/255/4,151/255/4,1/4) else gui:SetColor(0,0,0) end gui:DrawRect(pos.x,pos.y,sz.width,sz.height,1) --Draw text selection background if self.sellen~=0 then local n local x = gui:GetScale()*self.textindent local px = x local fragment = self:GetSelectedText() local w = gui:GetTextWidth(fragment) local c1 = math.min(self.caretposition,self.caretposition+self.sellen) local c2 = math.max(self.caretposition,self.caretposition+self.sellen) local prefix = String:Left(text,c1) px = px + gui:GetTextWidth(prefix) gui:SetColor(0.4,0.4,0.4) gui:DrawRect(pos.x + px + self.offsetx, pos.y+2*scale,w,sz.height-4*scale,0) end --Draw text gui:SetColor(0.75,0.75,0.75) if text~="" then gui:DrawText(text,scale * self.textindent + pos.x+self.offsetx,pos.y,math.max(sz.width,sz.width-self.offsetx),sz.height,Text.Left+Text.VCenter) end --Draw the caret if self.cursorblinkmode then if self.focused then local x = self:GetCaretCoord() gui:DrawLine(pos.x + x + self.offsetx,pos.y+2*scale,pos.x + x + self.offsetx,pos.y + sz.height-4*scale) end end end
  18. Josh
    An update is available on the beta branch. This includes all recent bug fixes, and is a full build with C++ libs for Windows and Linux.
     
    The following header search path needs to be added to your Visual Studio projects:
    $(LeadwerksHeaderPath)\Libraries\glslang
  19. Josh
    An update is available on the beta branch. This adds the Window::Center style back into the new refactored Window class, and adds a missing header file in the Professional Edition.
     
    If you'd like to try out a basic implementation of the new GUI, download and extract the attached scripts:
    Scripts.zip
     
    This code will create a GUI on the rendering context. It simply consists of a solid background filling the screen and a textless button.

    --Initialize Steamworks (optional) Steamworks:Initialize() --Set the application title title="test" --Create a window local windowstyle = window.Titlebar + window.Resizable if System:GetProperty("fullscreen")=="1" then windowstyle=windowstyle+window.FullScreen end window=Window:Create(title,0,0,System:GetProperty("screenwidth","1024"),System:GetProperty("screenheight","768"),windowstyle) --window:HideMouse() --Create the graphics context context=Context:Create(window,0) if context==nil then return end --Create a world world=World:Create() world:SetLightQuality((System:GetProperty("lightquality","1"))) --Load a map local mapfile = System:GetProperty("map","Maps/start.map") if Map:Load(mapfile)==false then return end --Create a GUI local gui = GUI:Create(context) gui:GetBase():SetScript("Scripts/GUI/Panel.lua") local widget = Widget:Create(20,20,75,30,gui:GetBase()) widget:SetScript("Scripts/GUI/Button.lua") while window:KeyDown(Key.Escape)==false do --If window has been closed, end the program if window:Closed() then break end --Handle map change if changemapname~=nil then --Clear all entities world:Clear() --Load the next map Time:Pause() if Map:Load("Maps/"..changemapname..".map")==false then return end Time:Resume() changemapname = nil end --Update the app timing Time:Update() --Update the world world:Update() --Render the world world:Render() --Render statistics context:SetBlendMode(Blend.Alpha) if DEBUG then context:SetColor(1,0,0,1) context:DrawText("Debug Mode",2,2) context:SetColor(1,1,1,1) context:DrawStats(2,22) context:SetBlendMode(Blend.Solid) else --Toggle statistics on and off if (window:KeyHit(Key.F11)) then showstats = not showstats end if showstats then context:SetColor(1,1,1,1) context:DrawText("FPS: "..Math:Round(Time:UPS()),2,2) end end --Refresh the screen context:Sync(true) end
     
    This code will create the same GUI directly on the window, with no rendering context initialized, using the native OS drawing commands:

    --Initialize Steamworks (optional) Steamworks:Initialize() --Set the application title title="test" --Create a window local windowstyle = window.Titlebar + window.Resizable if System:GetProperty("fullscreen")=="1" then windowstyle=windowstyle+window.FullScreen end window=Window:Create(title,0,0,System:GetProperty("screenwidth","1024"),System:GetProperty("screenheight","768"),windowstyle) --window:HideMouse() --Create a GUI local gui = GUI:Create(window) gui:GetBase():SetScript("Scripts/GUI/Panel.lua") local widget = Widget:Create(20,20,75,30,gui:GetBase()) widget:SetScript("Scripts/GUI/Button.lua") while window:KeyDown(Key.Escape)==false do --If window has been closed, end the program if window:Closed() then break end end
     
    The second example isn't really useful for games, but it will allow me to build a cross-platform UI for our own tools, and create an official GUI for in-game use, all in one step.
  20. Josh
    Yesterday I tried a plugin I've been meaning to try out for a long time, Steam Sign-in for IPB. It worked without a hitch and I was able to easily figure out how to locate a Steam ID from a forum account, and vice versa. You can sign straight into Leadwerks using your Steam ID, or you can add a Steam ID to your existing account. (See "Edit Profile".)
     
    The gallery, videos, Workshop, and games sections will now correctly link to your Leadwerks profile page. Notice when you click on the author's name, it now navigates to their forum profile page:
    http://www.leadwerks.com/werkspace/page/viewitem?fileid=634076765
     
    This also will allow me to add sections in your profile to show all your screenshots, videos, Workshop items, and games. A Steam badge will be shown next to your forum posts that links to your Steam profile.
     
    But wait, there's more we can do with this. Here are some of the ideas I've wanted to try for a long time:
    Display statistics like hours played and boxes created next to your forum posts.
    Display your achievements in the forum (and add some more!)
    Add achievements for forum posts, blogs, etc.
    Add achievements that unlock free custom assets.
    Gamify the game creation and learning process. (Quizzes?)
    Gain more transparency on how people are actually using the engine.

     
    Basically, this allows us to integrate community interaction with the editor usage more closely. I'm not exactly sure how it will work out, but it allows us to try a lot of new ideas in the future.
  21. Josh
    In this series of blogs I am going to describe my implementation of AI for a new Workshop Store model, the Leadwerks turret. The model is shown below, without the weapon inserted.
     

     
    Before writing any code I first decided the main behaviors I wanted this script to involve.
    The turret should scan the area to identify an enemy (using the team ID to differentiate between enemy types) and choose the closest target. Unlike the soldier AI, the turret AI will not change targets when another enemy attacks it.
    If the target is lost or occluded, another target will be chosen.
    The turret will fire a continuous rapid stream of bullets whenever an enemy is in view. We want this to be an obstacle you have to take cover from, or you will quickly be killed.
    The turret can be killed by either incurring enough damage to bring the health down to zero, or by knocking it over to deactivate it. If the turret is damaged to the point of "death" it will shoot sparks out once and then emit a cloud of smoke. The smoke will be continuous, to clearly indicate the turret is inactive.
    The turret movement will not use the physics system. The model is relatively small and I can't think of any really useful reasons I would want the motion to interact with physics, so I'm not going to implement this.

     
    The first draft of the script uses the World:ForEachEntityInAABB() function to perform a search and find a target enemy within a certain range. I set a breakpoint at line 18 to confirm that the script was successfully finding the player: Notice that a distance check is used so the turret will locate the closest enemy.

    Script.target = nil--enemy to shoot Script.mode = "idle" Script.lastsearchtime=0 Script.searchfrequency = 500 Script.range = 20 function TurretSearchHook(entity,extra) if entity.script~=nil then if type(entity.script.health)=="number" then if entity.script.health>0 then local d = extra:GetDistance(entity) if d<extra.script.range then if extra.script.target~=nil then if extra.script:GetDistance(extra.script.target)>d then extra.script.target = entity.script end else extra.script.target = entity.script end end end end end end function Script:UpdateWorld() --Search for target if self.target==nil then local currenttime = Time:GetCurrent() if currenttime - self.lastsearchtime > self.searchfrequency then self.lastsearchtime = currenttime local pos = self.entity:GetPosition(true) local aabb = AABB(pos - Vec3(self.range), pos + Vec3(self.range)) self.entity.world:ForEachEntityInAABBDo(aabb,"TurretSearchHook",self.entity) end end end
  22. Josh
    The beta branch contains a small update which affects the SoldierAI and Projectile scripts. The SoldierAI script now has a modifiable field that lets you choose a prefab for the projectile his weapon fires. The default value is "Prefabs\Projectiles\tracer.pfb" so your existing instances of these objects will be unaffected.
     
    By making the projectile prefab a modifiable value, we can use different types of projectiles with this script. For example, a medieval archer could fire arrows by creating a new prefab using a model of an arrow with the projectile script attached to it. Other types of weapons can be set up like energy weapons or rocket launchers. For example, you could create a "chicken gun" by selecting a prefab for the projectile that used a chicken model with the MonsterAI script attached to it.
  23. Josh
    The Workshop interface is updated with new options to let you quickly find items to use in your games.
     

     
    When you buy an item, you will now be directed to the Steam purchase page in the editor interface itself, without having to sign into an external web browser.
     

×
×
  • Create New...